diff --git a/.gitignore b/.gitignore index 71ffc1c5bbb25..ee043c9a3e4b2 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ phpunit-watcher.yml .tool-versions test/storybook-playwright/test-results test/storybook-playwright/specs/__snapshots__ +test/storybook-playwright/specs/*-snapshots/** diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index 281ace04b141c..d9d62450c94bf 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -293,7 +293,6 @@ You can attach private selectors and actions to a public store: ```js // In packages/package1/store.js: -import { experiments as dataExperiments } from '@wordpress/data'; import { __experimentalHasContentRoleAttribute, ...selectors } from './selectors'; import { __experimentalToggleFeature, ...actions } from './selectors'; // The `lock` function is exported from the internal experiments.js file where @@ -340,9 +339,9 @@ function MyComponent() { // In packages/package1/index.js: import { lock } from './private-apis'; -export const experiments = {}; +export const privateApis = {}; /* Attach private data to the exported object */ -lock( experiments, { +lock( privateApis, { __experimentalCallback: function () {}, __experimentalReactComponent: function ExperimentalComponent() { return
; @@ -352,7 +351,7 @@ lock( experiments, { } ); // In packages/package2/index.js: -import { experiments } from '@wordpress/package1'; +import { privateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; const { diff --git a/docs/contributors/code/e2e/README.md b/docs/contributors/code/e2e/README.md index 968db5638a6a9..1776fde77baf6 100644 --- a/docs/contributors/code/e2e/README.md +++ b/docs/contributors/code/e2e/README.md @@ -36,6 +36,8 @@ xvfb-run -- npm run test:e2e:playwright -- --project=webkit ## Best practices +Read the [best practices](https://playwright.dev/docs/best-practices) guide for Playwright. +

Forbid `$`, use `locator` instead

@@ -45,26 +47,22 @@ In fact, any API that returns `ElementHandle` is [discouraged](https://playwrigh

Use accessible selectors

-Use the selector engine [role-selector](https://playwright.dev/docs/selectors#role-selector) to construct the query wherever possible. It enables us to write accessible queries without having to rely on internal implementations. The syntax should be straightforward and looks like this: +Use [`getByRole`](https://playwright.dev/docs/locators#locate-by-role) to construct the query wherever possible. It enables us to write accessible queries without having to rely on internal implementations. ```js -// Select a button with the accessible name "Hello World" (case-insensitive). -page.locator( 'role=button[name="Hello World"i]' ); - -// Using short-form API, the `name` is case-insensitive by default. +// Select a button which includes the accessible name "Hello World" (case-insensitive). page.getByRole( 'button', { name: 'Hello World' } ); ``` -It's recommended to append `i` to the name attribute to match it case-insensitively wherever it makes sense. It can also be chained with built-in selector engines to perform complex queries: +It can also be chained to perform complex queries: ```js -// Select a button with a name ends with `Back` and is visible on the screen. -page.locator( 'role=button[name=/Back$/] >> visible=true' ); -// Select a button with the (exact) name "View options" under `#some-section`. -page.locator( 'css=#some-section >> role=button[name="View options"]' ); +// Select an option with a name "Buttons" under the "Block Library" region. +page.getByRole( 'region', { name: 'Block Library' } ) + .getByRole( 'option', { name: 'Buttons' } ) ``` -See the [official documentation](https://playwright.dev/docs/selectors#role-selector) for more info on how to use them. +See the [official documentation](https://playwright.dev/docs/locators) for more info on how to use them.
diff --git a/docs/manifest.json b/docs/manifest.json index 580fc8c70e2e5..9d0954b6b4b44 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1061,6 +1061,12 @@ "markdown_source": "../packages/components/src/navigator/navigator-screen/README.md", "parent": "components" }, + { + "title": "NavigatorToParentButton", + "slug": "navigator-to-parent-button", + "markdown_source": "../packages/components/src/navigator/navigator-to-parent-button/README.md", + "parent": "components" + }, { "title": "Notice", "slug": "notice", diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index eecac6cf2a9d7..2e5f509f6dc48 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -466,6 +466,39 @@ supports: { } ``` +## dimensions + +_**Note:** Since WordPress 6.2._ + +- Type: `Object` +- Default value: null +- Subproperties: + - `minHeight`: type `boolean`, default value `false` + +This value signals that a block supports some of the CSS style properties related to dimensions. When it does, the block editor will show UI controls for the user to set their values if [the theme declares support](/docs/how-to-guides/themes/theme-json/#opt-in-into-ui-controls). + +```js +supports: { + dimensions: { + minHeight: true // Enable min height control. + } +} +``` + +When a block declares support for a specific dimensions property, its attributes definition is extended to include the `style` attribute. + +- `style`: attribute of `object` type with no default assigned. This is added when `minHeight` support is declared. It stores the custom values set by the user, e.g.: + +```js +attributes: { + style: { + dimensions: { + minHeight: "50vh" + } + } +} +``` + ## html - Type: `boolean` @@ -485,7 +518,7 @@ supports: { - Type: `boolean` - Default value: `true` -By default, all blocks will appear in the inserter, block transforms menu, Style Book, etc. To hide a block from all parts of the user interface so that it can only be inserted programatically, set `inserter` to `false`. +By default, all blocks will appear in the inserter, block transforms menu, Style Book, etc. To hide a block from all parts of the user interface so that it can only be inserted programmatically, set `inserter` to `false`. ```js supports: { @@ -536,6 +569,42 @@ supports: { } ``` +## position + +_**Note:** Since WordPress 6.2._ + +- Type: `Object` +- Default value: null +- Subproperties: + - `sticky`: type `boolean`, default value `false` + +This value signals that a block supports some of the CSS style properties related to position. When it does, the block editor will show UI controls for the user to set their values if [the theme declares support](/docs/how-to-guides/themes/theme-json/#opt-in-into-ui-controls). + +Note that sticky position controls are currently only available for blocks set at the root level of the document. Setting a block to the `sticky` position will stick the block to its most immediate parent when the user scrolls the page. + +```js +supports: { + position: { + sticky: true // Enable selecting sticky position. + } +} +``` + +When the block declares support for a specific position property, its attributes definition is extended to include the `style` attribute. + +- `style`: attribute of `object` type with no default assigned. This is added when `sticky` support is declared. It stores the custom values set by the user, e.g.: + +```js +attributes: { + style: { + position: { + type: "sticky", + top: "0px" + } + } +} +``` + ## spacing - Type: `Object` @@ -545,7 +614,7 @@ supports: { - `padding`: type `boolean` or `array`, default value `false` - `blockGap`: type `boolean` or `array`, default value `false` -This value signals that a block supports some of the CSS style properties related to spacing. When it does, the block editor will show UI controls for the user to set their values, if [the theme declares support](/docs/how-to-guides/themes/theme-support.md#cover-block-padding). +This value signals that a block supports some of the CSS style properties related to spacing. When it does, the block editor will show UI controls for the user to set their values if [the theme declares support](/docs/how-to-guides/themes/theme-support.md#cover-block-padding). ```js supports: { @@ -557,7 +626,7 @@ supports: { } ``` -When the block declares support for a specific spacing property, the attributes definition is extended to include the `style` attribute. +When the block declares support for a specific spacing property, its attributes definition is extended to include the `style` attribute. - `style`: attribute of `object` type with no default assigned. This is added when `margin` or `padding` support is declared. It stores the custom values set by the user, e.g.: diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 22171c32d6088..2917c8577b07d 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -320,7 +320,7 @@ Display a list of your most recent comments. ([Source](https://github.com/WordPr - **Name:** core/latest-comments - **Category:** widgets -- **Supports:** align, anchor, spacing (margin, padding), ~~html~~ +- **Supports:** align, anchor, spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** commentsToShow, displayAvatar, displayDate, displayExcerpt ## Latest Posts diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 118327a1f8b0d..acc0a499ec61b 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -23,6 +23,7 @@ Setting that enables the following UI tools: - border: color, radius, style, width - color: link - dimensions: minHeight +- position: sticky - spacing: blockGap, margin, padding - typography: lineHeight diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 7697f610af86d..1c8de2e2c3412 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -380,14 +380,9 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { return (string) $content; } - $global_settings = gutenberg_get_global_settings(); - $block_gap = _wp_array_get( $global_settings, array( 'spacing', 'blockGap' ), null ); - $has_block_gap_support = isset( $block_gap ); - $global_layout_settings = _wp_array_get( $global_settings, array( 'layout' ), null ); - $root_padding_aware_alignments = _wp_array_get( $global_settings, array( 'useRootPaddingAwareAlignments' ), false ); - - $default_block_layout = _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() ); - $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $default_block_layout; + $global_settings = gutenberg_get_global_settings(); + $global_layout_settings = _wp_array_get( $global_settings, array( 'layout' ), null ); + $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() ); if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] && ! $global_layout_settings ) { return $block_content; @@ -403,6 +398,8 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $used_layout['type'] = 'constrained'; } + $root_padding_aware_alignments = _wp_array_get( $global_settings, array( 'useRootPaddingAwareAlignments' ), false ); + if ( $root_padding_aware_alignments && isset( $used_layout['type'] ) && @@ -470,6 +467,9 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { */ $should_skip_gap_serialization = wp_should_skip_block_supports_serialization( $block_type, 'spacing', 'blockGap' ); + $block_gap = _wp_array_get( $global_settings, array( 'spacing', 'blockGap' ), null ); + $has_block_gap_support = isset( $block_gap ); + $style = gutenberg_get_layout_style( ".$container_class.$container_class", $used_layout, diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 02ace2860990e..1e828edcaddc3 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -785,6 +785,9 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n * @return string The new selector. */ protected static function append_to_selector( $selector, $to_append, $position = 'right' ) { + if ( ! str_contains( ',', $selector ) ) { + return 'right' === $position ? $selector . $to_append : $to_append . $selector; + } $new_selectors = array(); $selectors = explode( ',', $selector ); foreach ( $selectors as $sel ) { @@ -2513,9 +2516,9 @@ public function get_root_layout_rules( $selector, $block_metadata ) { // The above rule is negated for alignfull children of nested containers. $css .= '.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }'; // Some of the children of alignfull blocks without content width should also get padding: text blocks and non-alignfull container blocks. - $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; + $css .= '.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }'; // The above rule also has to be negated for blocks inside nested `.has-global-padding` blocks. - $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; + $css .= '.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }'; } $css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; diff --git a/lib/compat/wordpress-6.1/block-template-utils.php b/lib/compat/wordpress-6.1/block-template-utils.php index 928291b500084..5e240d227d358 100644 --- a/lib/compat/wordpress-6.1/block-template-utils.php +++ b/lib/compat/wordpress-6.1/block-template-utils.php @@ -76,11 +76,12 @@ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_t $post_type = isset( $query['post_type'] ) ? $query['post_type'] : ''; $wp_query_args = array( - 'post_status' => array( 'auto-draft', 'draft', 'publish' ), - 'post_type' => $template_type, - 'posts_per_page' => -1, - 'no_found_rows' => true, - 'tax_query' => array( + 'post_status' => array( 'auto-draft', 'draft', 'publish' ), + 'post_type' => $template_type, + 'posts_per_page' => -1, + 'no_found_rows' => true, + 'lazy_load_term_meta' => false, + 'tax_query' => array( array( 'taxonomy' => 'wp_theme', 'field' => 'name', @@ -147,7 +148,7 @@ function gutenberg_get_block_templates( $query = array(), $template_type = 'wp_t } $is_not_custom = false === array_search( - wp_get_theme()->get_stylesheet() . '//' . $template_file['slug'], + get_stylesheet() . '//' . $template_file['slug'], array_column( $query_result, 'id' ), true ); diff --git a/lib/compat/wordpress-6.1/blocks.php b/lib/compat/wordpress-6.1/blocks.php index 3aa790dadcb1c..e8093961896a4 100644 --- a/lib/compat/wordpress-6.1/blocks.php +++ b/lib/compat/wordpress-6.1/blocks.php @@ -323,28 +323,3 @@ function gutenberg_block_type_metadata_render_template( $settings, $metadata ) { return $settings; } add_filter( 'block_type_metadata_settings', 'gutenberg_block_type_metadata_render_template', 10, 2 ); - -/** - * Registers the metadata block attribute for block types. - * - * Once 6.1 is the minimum supported WordPress version for the Gutenberg - * plugin, this shim can be removed - * - * @param array $args Array of arguments for registering a block type. - * @return array $args - */ -function gutenberg_register_metadata_attribute( $args ) { - // Setup attributes if needed. - if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { - $args['attributes'] = array(); - } - - if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) { - $args['attributes']['metadata'] = array( - 'type' => 'object', - ); - } - - return $args; -} -add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); diff --git a/lib/compat/wordpress-6.2/rest-api.php b/lib/compat/wordpress-6.2/rest-api.php index 06780aceb245e..a504be4dca2a6 100644 --- a/lib/compat/wordpress-6.2/rest-api.php +++ b/lib/compat/wordpress-6.2/rest-api.php @@ -5,21 +5,6 @@ * @package gutenberg */ -/** - * Update `wp_template` and `wp_template-part` post types to use - * Gutenberg's REST controller. - * - * @param array $args Array of arguments for registering a post type. - * @param string $post_type Post type key. - */ -function gutenberg_update_templates_template_parts_rest_controller( $args, $post_type ) { - if ( in_array( $post_type, array( 'wp_template', 'wp_template-part' ), true ) ) { - $args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller_6_2'; - } - return $args; -} -add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 ); - /** * Registers the block pattern categories REST API routes. */ diff --git a/lib/compat/wordpress-6.2/site-editor.php b/lib/compat/wordpress-6.2/site-editor.php index b6246e49c6d11..37087fae3fb62 100644 --- a/lib/compat/wordpress-6.2/site-editor.php +++ b/lib/compat/wordpress-6.2/site-editor.php @@ -22,3 +22,24 @@ function gutenberg_site_editor_unset_homepage_setting( $settings, $context ) { return $settings; } add_filter( 'block_editor_settings_all', 'gutenberg_site_editor_unset_homepage_setting', 10, 2 ); + +/** + * Overrides the site editor initialization for WordPress 6.2 and cancels the redirection. + * The logic of this function is not important, we just need to remove the redirection from core. + * + * @param string $location Location. + * + * @return string Updated location. + */ +function gutenberg_prevent_site_editor_redirection( $location ) { + if ( strpos( $location, 'site-editor.php' ) !== false && strpos( $location, '?' ) !== false ) { + return add_query_arg( + array( 'postId' => 'none' ), + admin_url( 'site-editor.php' ) + ); + } + + return $location; +} + +add_filter( 'wp_redirect', 'gutenberg_prevent_site_editor_redirection', 1 ); diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-templates-controller-6-2.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php similarity index 97% rename from lib/compat/wordpress-6.2/class-gutenberg-rest-templates-controller-6-2.php rename to lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php index 16d38ac073e68..f857cedd37f9a 100644 --- a/lib/compat/wordpress-6.2/class-gutenberg-rest-templates-controller-6-2.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php @@ -9,7 +9,7 @@ /** * Base Templates REST API Controller. */ -class Gutenberg_REST_Templates_Controller_6_2 extends Gutenberg_REST_Templates_Controller { +class Gutenberg_REST_Templates_Controller_6_3 extends Gutenberg_REST_Templates_Controller { /** * Registers the controllers routes. diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index 55a45df07d593..93422289100df 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -13,3 +13,18 @@ function gutenberg_register_rest_pattern_directory() { $pattern_directory_controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_rest_pattern_directory' ); + +/** + * Update `wp_template` and `wp_template-part` post types to use + * Gutenberg's REST controller. + * + * @param array $args Array of arguments for registering a post type. + * @param string $post_type Post type key. + */ +function gutenberg_update_templates_template_parts_rest_controller( $args, $post_type ) { + if ( in_array( $post_type, array( 'wp_template', 'wp_template-part' ), true ) ) { + $args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller_6_3'; + } + return $args; +} +add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 ); diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index b789deaba6ed5..a33d2f6b98012 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -77,3 +77,26 @@ function wp_enqueue_block_view_script( $block_name, $args ) { add_filter( 'render_block', $callback, 10, 2 ); } } + + +/** + * Registers the metadata block attribute for block types. + * + * @param array $args Array of arguments for registering a block type. + * @return array $args + */ +function gutenberg_register_metadata_attribute( $args ) { + // Setup attributes if needed. + if ( ! isset( $args['attributes'] ) || ! is_array( $args['attributes'] ) ) { + $args['attributes'] = array(); + } + + if ( ! array_key_exists( 'metadata', $args['attributes'] ) ) { + $args['attributes']['metadata'] = array( + 'type' => 'object', + ); + } + + return $args; +} +add_filter( 'register_block_type_args', 'gutenberg_register_metadata_attribute' ); diff --git a/lib/load.php b/lib/load.php index dc253408a9289..035fbe3740627 100644 --- a/lib/load.php +++ b/lib/load.php @@ -44,13 +44,13 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php'; require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php'; require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php'; - require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-templates-controller-6-2.php'; require_once __DIR__ . '/compat/wordpress-6.2/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.2/block-patterns.php'; require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php'; // WordPress 6.3 compat. require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-pattern-directory-controller-6-3.php'; + require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php'; require_once __DIR__ . '/compat/wordpress-6.3/rest-api.php'; // Experimental. diff --git a/package-lock.json b/package-lock.json index 1749ed01a7cc2..6fb9ece91f420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7941,19 +7941,19 @@ } }, "@playwright/test": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz", - "integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz", + "integrity": "sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==", "dev": true, "requires": { "@types/node": "*", - "playwright-core": "1.27.1" + "playwright-core": "1.30.0" }, "dependencies": { "playwright-core": { - "version": "1.27.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz", - "integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz", + "integrity": "sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==", "dev": true } } @@ -17723,6 +17723,7 @@ "@wordpress/core-data": "file:packages/core-data", "@wordpress/data": "file:packages/data", "@wordpress/deprecated": "file:packages/deprecated", + "@wordpress/dom": "^3.20.0", "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", "@wordpress/hooks": "file:packages/hooks", @@ -17744,6 +17745,36 @@ "lodash": "^4.17.21", "memize": "^1.1.0", "rememo": "^4.0.0" + }, + "dependencies": { + "@wordpress/dom": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-3.20.0.tgz", + "integrity": "sha512-Q35qCW8jj/JXTTujcC0wDDaLNNdzivkWkvCQj9FTyb+SoT8ZMwcwipnNvhyC0qprPlEREdQtNODmlSm4Ur4YkA==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/deprecated": "^3.20.0" + }, + "dependencies": { + "@wordpress/deprecated": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-3.20.0.tgz", + "integrity": "sha512-JruZLx74uP9uI5qi7uTANMXwLBNtGHvw/pYCtWBaisFTUutGm1fF1tGWFtIgTy5j1SjHtN0PMkTFlvdpJ+HASQ==", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/hooks": "^3.20.0" + } + }, + "@wordpress/hooks": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.20.0.tgz", + "integrity": "sha512-OMOJwmbubrKueXhXEyBNU8CXBycawmtXCWbhqgYYbihgecB7cSZ1kAAPz+Oi/5j+3+XDfSlZXgWM1lCwvfnzPQ==", + "requires": { + "@babel/runtime": "^7.16.0" + } + } + } + } } }, "@wordpress/edit-site": { @@ -19332,7 +19363,7 @@ "app-root-dir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", - "integrity": "sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg=", + "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==", "dev": true }, "app-root-path": { @@ -25990,7 +26021,7 @@ "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", "dev": true }, "array-includes": { @@ -27625,7 +27656,7 @@ "babel-plugin-add-react-displayname": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", - "integrity": "sha1-M51M3be2X9YtHfnbn+BN4TQSK9U=", + "integrity": "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw==", "dev": true }, "babel-plugin-apply-mdx-type-prop": { @@ -27997,7 +28028,7 @@ "batch-processor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz", - "integrity": "sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg=", + "integrity": "sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==", "dev": true }, "bcrypt-pbkdf": { @@ -31090,7 +31121,7 @@ "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", "dev": true }, "cssesc": { @@ -31314,7 +31345,7 @@ "debuglog": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=", "dev": true }, "decache": { @@ -36246,7 +36277,7 @@ "git-remote-origin-url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", "dev": true, "requires": { "gitconfiglocal": "^1.0.0", @@ -36293,7 +36324,7 @@ "gitconfiglocal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", "dev": true, "requires": { "ini": "^1.3.2" @@ -36332,7 +36363,7 @@ "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { "is-extglob": "^2.1.0" @@ -36641,7 +36672,7 @@ "has-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", - "integrity": "sha1-mqqe7b/7G6OZCnsAEPtnjuAIEgc=", + "integrity": "sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==", "dev": true, "requires": { "is-glob": "^3.0.0" @@ -36650,7 +36681,7 @@ "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "dev": true, "requires": { "is-extglob": "^2.1.0" @@ -37495,7 +37526,7 @@ "humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", "dev": true, "requires": { "ms": "^2.0.0" @@ -38512,7 +38543,7 @@ "is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", "dev": true, "requires": { "text-extensions": "^1.0.0" @@ -38824,7 +38855,7 @@ "is-window": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz", - "integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0=", + "integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg==", "dev": true }, "is-windows": { @@ -42224,7 +42255,7 @@ "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", - "integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", "dev": true }, "js-tokens": { @@ -42615,7 +42646,7 @@ "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, "jsprim": { @@ -43696,7 +43727,7 @@ "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", "dev": true }, "lodash.isplainobject": { @@ -43955,7 +43986,7 @@ "lz-string": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", - "integrity": "sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", "dev": true }, "macos-release": { @@ -47262,7 +47293,7 @@ "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", "dev": true }, "number-is-nan": { @@ -48730,7 +48761,7 @@ "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", "dev": true }, "p-event": { @@ -50286,7 +50317,7 @@ "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", "dev": true }, "private": { @@ -50845,7 +50876,7 @@ "promzard": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", - "integrity": "sha512-JZeYqd7UAcHCwI+sTOeUDYkvEU+1bQ7iE0UT1MgB/tERkAPkesW46MrpIySzODi+owTjZtiF8Ay5j9m60KmMBw==", + "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", "dev": true, "requires": { "read": "1" @@ -50879,7 +50910,7 @@ "proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true }, "protocols": { @@ -52224,7 +52255,7 @@ "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", "dev": true, "requires": { "mute-stream": "~0.0.4" @@ -52783,7 +52814,7 @@ "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true }, "remark": { @@ -57844,7 +57875,7 @@ "temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", - "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", "dev": true }, "terminal-link": { diff --git a/package.json b/package.json index d49bb29104aec..aaf16e907f13e 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.6.0", - "@playwright/test": "1.27.1", + "@playwright/test": "1.30.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.2", "@storybook/addon-a11y": "6.5.7", "@storybook/addon-actions": "6.5.7", diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 19433e516f4f5..18c2c2cfbce52 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -348,10 +348,6 @@ _Returns_ Undocumented declaration. -### experiments - -Experimental @wordpress/block-editor APIs. - ### FontSizePicker _Related_ @@ -639,6 +635,10 @@ _Related_ - +### privateApis + +Private @wordpress/block-editor APIs. + ### RichText _Related_ diff --git a/packages/block-editor/src/components/block-alignment-control/use-available-alignments.js b/packages/block-editor/src/components/block-alignment-control/use-available-alignments.js index ab6213b438928..d77eb2af2a597 100644 --- a/packages/block-editor/src/components/block-alignment-control/use-available-alignments.js +++ b/packages/block-editor/src/components/block-alignment-control/use-available-alignments.js @@ -19,20 +19,25 @@ export default function useAvailableAlignments( controls = DEFAULT_CONTROLS ) { if ( ! controls.includes( 'none' ) ) { controls = [ 'none', ...controls ]; } - const { wideControlsEnabled = false, themeSupportsLayout } = useSelect( - ( select ) => { - const { getSettings } = select( blockEditorStore ); - const settings = getSettings(); - return { - wideControlsEnabled: settings.alignWide, - themeSupportsLayout: settings.supportsLayout, - }; - }, - [] - ); + const { + wideControlsEnabled = false, + themeSupportsLayout, + isBlockBasedTheme, + } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + const settings = getSettings(); + return { + wideControlsEnabled: settings.alignWide, + themeSupportsLayout: settings.supportsLayout, + isBlockBasedTheme: settings.__unstableIsBlockBasedTheme, + }; + }, [] ); const layout = useLayout(); const layoutType = getLayoutType( layout?.type ); - const layoutAlignments = layoutType.getAlignments( layout ); + const layoutAlignments = layoutType.getAlignments( + layout, + isBlockBasedTheme + ); if ( themeSupportsLayout ) { const alignments = layoutAlignments.filter( diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 5b2151dbfaba4..b843b978bbd4b 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -218,3 +218,48 @@ export function useSupportedStyles( name, element ) { return supportedPanels; } + +/** + * Given a settings object and a list of supported panels, + * returns a new settings object with the unsupported panels removed. + * + * @param {Object} settings Settings object. + * @param {string[]} supports Supported style panels. + * + * @return {Object} Merge of settings and supports. + */ +export function overrideSettingsWithSupports( settings, supports ) { + const updatedSettings = { ...settings }; + + if ( ! supports.includes( 'fontSize' ) ) { + updatedSettings.typography = { + ...updatedSettings.typography, + fontSizes: {}, + customFontSize: false, + }; + } + + if ( ! supports.includes( 'fontFamily' ) ) { + updatedSettings.typography = { + ...updatedSettings.typography, + fontFamilies: {}, + }; + } + + [ + 'lineHeight', + 'fontStyle', + 'fontWeight', + 'letterSpacing', + 'textTransform', + ].forEach( ( key ) => { + if ( ! supports.includes( key ) ) { + updatedSettings.typography = { + ...updatedSettings.typography, + [ key ]: false, + }; + } + } ); + + return updatedSettings; +} diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 6581f46254985..77582d2f05415 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -2,6 +2,7 @@ export { useGlobalStylesReset, useGlobalSetting, useGlobalStyle, + overrideSettingsWithSupports, } from './hooks'; export { useGlobalStylesOutput } from './use-global-styles-output'; export { GlobalStylesContext } from './context'; diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index 471055be391f7..15ebf70da93a8 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -17,29 +17,16 @@ import LineHeightControl from '../line-height-control'; import LetterSpacingControl from '../letter-spacing-control'; import TextTransformControl from '../text-transform-control'; import TextDecorationControl from '../text-decoration-control'; -import { useSupportedStyles } from './hooks'; import { getValueFromVariable } from './utils'; -export function useHasTypographyPanel( name, element, settings ) { - const hasFontFamily = useHasFontFamilyControl( name, element, settings ); - const hasLineHeight = useHasLineHeightControl( name, element, settings ); - const hasFontAppearance = useHasAppearanceControl( - name, - element, - settings - ); - const hasLetterSpacing = useHasLetterSpacingControl( - name, - element, - settings - ); - const hasTextTransform = useHasTextTransformControl( - name, - element, - settings - ); - const hasTextDecoration = useHasTextDecorationControl( name, element ); - const hasFontSize = useHasFontSizeControl( name, element, settings ); +export function useHasTypographyPanel( settings ) { + const hasFontFamily = useHasFontFamilyControl( settings ); + const hasLineHeight = useHasLineHeightControl( settings ); + const hasFontAppearance = useHasAppearanceControl( settings ); + const hasLetterSpacing = useHasLetterSpacingControl( settings ); + const hasTextTransform = useHasTextTransformControl( settings ); + const hasTextDecoration = useHasTextDecorationControl( settings ); + const hasFontSize = useHasFontSizeControl( settings ); return ( hasFontFamily || @@ -52,52 +39,38 @@ export function useHasTypographyPanel( name, element, settings ) { ); } -function useHasFontSizeControl( name, element, settings ) { - const supports = useSupportedStyles( name, element ); +function useHasFontSizeControl( settings ) { const disableCustomFontSizes = ! settings?.typography?.customFontSize; const fontSizesPerOrigin = settings?.typography?.fontSizes ?? {}; const fontSizes = fontSizesPerOrigin?.custom ?? fontSizesPerOrigin?.theme ?? fontSizesPerOrigin.default; - return ( - supports.includes( 'fontSize' ) && - ( !! fontSizes?.length || ! disableCustomFontSizes ) - ); + return !! fontSizes?.length || ! disableCustomFontSizes; } -function useHasFontFamilyControl( name, element, settings ) { - const supports = useSupportedStyles( name, element ); +function useHasFontFamilyControl( settings ) { const fontFamiliesPerOrigin = settings?.typography?.fontFamilies; const fontFamilies = fontFamiliesPerOrigin?.custom ?? fontFamiliesPerOrigin?.theme ?? fontFamiliesPerOrigin?.default; - return supports.includes( 'fontFamily' ) && !! fontFamilies?.length; + return !! fontFamilies?.length; } -function useHasLineHeightControl( name, element, settings ) { - const supports = useSupportedStyles( name, element ); - return ( - settings?.typography?.lineHeight && supports.includes( 'lineHeight' ) - ); +function useHasLineHeightControl( settings ) { + return settings?.typography?.lineHeight; } -function useHasAppearanceControl( name, element, settings ) { - const supports = useSupportedStyles( name, element ); - const hasFontStyles = - settings?.typography?.fontStyle && supports.includes( 'fontStyle' ); - const hasFontWeights = - settings?.typography?.fontWeight && supports.includes( 'fontWeight' ); +function useHasAppearanceControl( settings ) { + const hasFontStyles = settings?.typography?.fontStyle; + const hasFontWeights = settings?.typography?.fontWeight; return hasFontStyles || hasFontWeights; } -function useAppearanceControlLabel( name, element, settings ) { - const supports = useSupportedStyles( name, element ); - const hasFontStyles = - settings?.typography?.fontStyle && supports.includes( 'fontStyle' ); - const hasFontWeights = - settings?.typography?.fontWeight && supports.includes( 'fontWeight' ); +function useAppearanceControlLabel( settings ) { + const hasFontStyles = settings?.typography?.fontStyle; + const hasFontWeights = settings?.typography?.fontWeight; if ( ! hasFontStyles ) { return __( 'Font weight' ); } @@ -107,27 +80,16 @@ function useAppearanceControlLabel( name, element, settings ) { return __( 'Appearance' ); } -function useHasLetterSpacingControl( name, element, settings ) { - const setting = settings?.typography?.letterSpacing; - const supports = useSupportedStyles( name, element ); - if ( ! setting ) { - return false; - } - return supports.includes( 'letterSpacing' ); +function useHasLetterSpacingControl( settings ) { + return settings?.typography?.letterSpacing; } -function useHasTextTransformControl( name, element, settings ) { - const setting = settings?.typography?.textTransform; - const supports = useSupportedStyles( name, element ); - if ( ! setting ) { - return false; - } - return supports.includes( 'textTransform' ); +function useHasTextTransformControl( settings ) { + return settings?.typography?.textTransform; } -function useHasTextDecorationControl( name, element ) { - const supports = useSupportedStyles( name, element ); - return supports.includes( 'textDecoration' ); +function useHasTextDecorationControl( settings ) { + return settings?.typography?.textDecoration; } function TypographyToolsPanel( { ...props } ) { @@ -146,8 +108,6 @@ const DEFAULT_CONTROLS = { export default function TypographyPanel( { as: Wrapper = TypographyToolsPanel, - name, - element, value, onChange, inheritedValue = value, @@ -159,11 +119,7 @@ export default function TypographyPanel( { getValueFromVariable( { settings }, '', rawValue ); // Font Family - const hasFontFamilyEnabled = useHasFontFamilyControl( - name, - element, - settings - ); + const hasFontFamilyEnabled = useHasFontFamilyControl( settings ); const fontFamiliesPerOrigin = settings?.typography?.fontFamilies; const fontFamilies = fontFamiliesPerOrigin?.custom ?? @@ -188,7 +144,7 @@ export default function TypographyPanel( { const resetFontFamily = () => setFontFamily( undefined ); // Font Size - const hasFontSizeEnabled = useHasFontSizeControl( name, element, settings ); + const hasFontSizeEnabled = useHasFontSizeControl( settings ); const disableCustomFontSizes = ! settings?.typography?.customFontSize; const fontSizesPerOrigin = settings?.typography?.fontSizes ?? {}; const fontSizes = @@ -213,16 +169,8 @@ export default function TypographyPanel( { const resetFontSize = () => setFontSize( undefined ); // Appearance - const hasAppearanceControl = useHasAppearanceControl( - name, - element, - settings - ); - const appearanceControlLabel = useAppearanceControlLabel( - name, - element, - settings - ); + const hasAppearanceControl = useHasAppearanceControl( settings ); + const appearanceControlLabel = useAppearanceControlLabel( settings ); const hasFontStyles = settings?.typography?.fontStyle; const hasFontWeights = settings?.typography?.fontWeight; const fontStyle = decodeValue( inheritedValue?.typography?.fontStyle ); @@ -247,11 +195,7 @@ export default function TypographyPanel( { }; // Line Height - const hasLineHeightEnabled = useHasLineHeightControl( - name, - element, - settings - ); + const hasLineHeightEnabled = useHasLineHeightControl( settings ); const lineHeight = decodeValue( inheritedValue?.typography?.lineHeight ); const setLineHeight = ( newValue ) => { onChange( { @@ -266,11 +210,7 @@ export default function TypographyPanel( { const resetLineHeight = () => setLineHeight( undefined ); // Letter Spacing - const hasLetterSpacingControl = useHasLetterSpacingControl( - name, - element, - settings - ); + const hasLetterSpacingControl = useHasLetterSpacingControl( settings ); const letterSpacing = decodeValue( inheritedValue?.typography?.letterSpacing ); @@ -287,11 +227,7 @@ export default function TypographyPanel( { const resetLetterSpacing = () => setLetterSpacing( undefined ); // Text Transform - const hasTextTransformControl = useHasTextTransformControl( - name, - element, - settings - ); + const hasTextTransformControl = useHasTextTransformControl( settings ); const textTransform = decodeValue( inheritedValue?.typography?.textTransform ); @@ -308,10 +244,7 @@ export default function TypographyPanel( { const resetTextTransform = () => setTextTransform( undefined ); // Text Decoration - const hasTextDecorationControl = useHasTextDecorationControl( - name, - element - ); + const hasTextDecorationControl = useHasTextDecorationControl( settings ); const textDecoration = decodeValue( inheritedValue?.typography?.textDecoration ); diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index d4a5512167260..da518889223e2 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -664,13 +664,17 @@ export const toStyles = ( } if ( useRootPaddingAlign ) { + /* + * These rules reproduce the ones from https://github.com/WordPress/gutenberg/blob/79103f124925d1f457f627e154f52a56228ed5ad/lib/class-wp-theme-json-gutenberg.php#L2508 + * almost exactly, but for the selectors that target block wrappers in the front end. This code only runs in the editor, so it doesn't need those selectors. + */ ruleset += `padding-right: 0; padding-left: 0; padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom) } .has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); } .has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; } .has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); } .has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; } - .has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); } - .has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0;`; + .has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where(.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); } + .has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where(.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0;`; } ruleset += '}'; diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 7166b594c276c..5425290809a18 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -174,6 +174,11 @@ function ListView( [ isMounted.current, draggedClientIds, expandedState, expand, collapse ] ); + // If there are no blocks to show, do not render the list view. + if ( ! clientIdsTree.length ) { + return null; + } + return ( setIsLinkUIOpen( false ) } /> ) } diff --git a/packages/block-editor/src/components/off-canvas-editor/leaf-more-menu.js b/packages/block-editor/src/components/off-canvas-editor/leaf-more-menu.js index 6b4d8cf893382..a266f0d9377c3 100644 --- a/packages/block-editor/src/components/off-canvas-editor/leaf-more-menu.js +++ b/packages/block-editor/src/components/off-canvas-editor/leaf-more-menu.js @@ -19,11 +19,68 @@ const POPOVER_PROPS = { variant: 'toolbar', }; +const BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU = [ + 'core/navigation-link', + 'core/navigation-submenu', +]; + +function AddSubmenuItem( { block, onClose } ) { + const { insertBlock, replaceBlock, replaceInnerBlocks } = + useDispatch( blockEditorStore ); + + const clientId = block.clientId; + const isDisabled = ! BLOCKS_THAT_CAN_BE_CONVERTED_TO_SUBMENU.includes( + block.name + ); + return ( + { + const updateSelectionOnInsert = false; + const newLink = createBlock( 'core/navigation-link' ); + + if ( block.name === 'core/navigation-submenu' ) { + insertBlock( + newLink, + block.innerBlocks.length, + clientId, + updateSelectionOnInsert + ); + } else { + // Convert to a submenu if the block currently isn't one. + const newSubmenu = createBlock( + 'core/navigation-submenu', + block.attributes, + block.innerBlocks + ); + + // The following must happen as two independent actions. + // Why? Because the offcanvas editor relies on the getLastInsertedBlocksClientIds + // selector to determine which block is "active". As the UX needs the newLink to be + // the "active" block it must be the last block to be inserted. + // Therefore the Submenu is first created and **then** the newLink is inserted + // thus ensuring it is the last inserted block. + replaceBlock( clientId, newSubmenu ); + + replaceInnerBlocks( + newSubmenu.clientId, + [ newLink ], + updateSelectionOnInsert + ); + } + onClose(); + } } + > + { __( 'Add submenu link' ) } + + ); +} + export default function LeafMoreMenu( props ) { const { clientId, block } = props; - const { insertBlock, replaceBlock, removeBlocks, replaceInnerBlocks } = - useDispatch( blockEditorStore ); + const { removeBlocks } = useDispatch( blockEditorStore ); const label = sprintf( /* translators: %s: block name */ @@ -42,47 +99,7 @@ export default function LeafMoreMenu( props ) { > { ( { onClose } ) => ( - { - const updateSelectionOnInsert = false; - const newLink = createBlock( - 'core/navigation-link' - ); - if ( block.name === 'core/navigation-submenu' ) { - insertBlock( - newLink, - block.innerBlocks.length, - clientId, - updateSelectionOnInsert - ); - } else { - // Convert to a submenu if the block currently isn't one. - const newSubmenu = createBlock( - 'core/navigation-submenu', - block.attributes, - block.innerBlocks - ); - - // The following must happen as two independent actions. - // Why? Because the offcanvas editor relies on the getLastInsertedBlocksClientIds - // selector to determine which block is "active". As the UX needs the newLink to be - // the "active" block it must be the last block to be inserted. - // Therefore the Submenu is first created and **then** the newLink is inserted - // thus ensuring it is the last inserted block. - replaceBlock( clientId, newSubmenu ); - - replaceInnerBlocks( - newSubmenu.clientId, - [ newLink ], - updateSelectionOnInsert - ); - } - onClose(); - } } - > - { __( 'Add submenu item' ) } - + { removeBlocks( [ clientId ], false ); diff --git a/packages/block-editor/src/components/off-canvas-editor/link-ui.js b/packages/block-editor/src/components/off-canvas-editor/link-ui.js index 695d8b62dda92..f6b5e2538d9e7 100644 --- a/packages/block-editor/src/components/off-canvas-editor/link-ui.js +++ b/packages/block-editor/src/components/off-canvas-editor/link-ui.js @@ -151,6 +151,7 @@ export function LinkUI( props ) { suggestionsQuery={ getSuggestionsQuery( type, kind ) } onChange={ props.onChange } onRemove={ props.onRemove } + onCancel={ props.onCancel } renderControlBottom={ ! url ? () => ( diff --git a/packages/block-editor/src/components/url-popover/index.js b/packages/block-editor/src/components/url-popover/index.js index 8c2bbec69c5de..071d2c7e00b8b 100644 --- a/packages/block-editor/src/components/url-popover/index.js +++ b/packages/block-editor/src/components/url-popover/index.js @@ -6,7 +6,7 @@ import { useState } from '@wordpress/element'; import { Button, Popover, - experiments as componentsExperiments, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { chevronDown } from '@wordpress/icons'; import deprecated from '@wordpress/deprecated'; @@ -19,7 +19,7 @@ import LinkEditor from './link-editor'; import { unlock } from '../../lock-unlock'; const { __experimentalPopoverLegacyPositionToPlacement } = unlock( - componentsExperiments + componentsPrivateApis ); const DEFAULT_PLACEMENT = 'bottom'; diff --git a/packages/block-editor/src/hooks/metadata.js b/packages/block-editor/src/hooks/metadata.js index 9f39082f46d85..918f4f80ee9c2 100644 --- a/packages/block-editor/src/hooks/metadata.js +++ b/packages/block-editor/src/hooks/metadata.js @@ -7,6 +7,10 @@ import { getBlockSupport } from '@wordpress/blocks'; const META_ATTRIBUTE_NAME = 'metadata'; export function hasBlockMetadataSupport( blockType, feature = '' ) { + // Only core blocks are allowed to use __experimentalMetadata until the fetaure is stablised. + if ( ! blockType.name.startsWith( 'core/' ) ) { + return false; + } const support = getBlockSupport( blockType, '__experimentalMetadata' ); return !! ( true === support || support?.[ feature ] ); } diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index c5176304fb958..2c5589c1920bb 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -10,7 +10,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; import { BaseControl, - experiments as componentsExperiments, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; @@ -32,7 +32,7 @@ import { cleanEmptyObject } from './utils'; import { unlock } from '../lock-unlock'; import { store as blockEditorStore } from '../store'; -const { CustomSelectControl } = unlock( componentsExperiments ); +const { CustomSelectControl } = unlock( componentsPrivateApis ); const POSITION_SUPPORT_KEY = 'position'; diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index 87e734b00667c..a5aac0cb2d312 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -18,6 +18,10 @@ import { FONT_FAMILY_SUPPORT_KEY } from './font-family'; import { FONT_SIZE_SUPPORT_KEY } from './font-size'; import { useSetting } from '../components'; import { cleanEmptyObject } from './utils'; +import { + overrideSettingsWithSupports, + useSupportedStyles, +} from '../components/global-styles/hooks'; function omit( object, keys ) { return Object.fromEntries( @@ -48,33 +52,59 @@ function TypographyInspectorControl( { children } ) { ); } +function useBlockSettings( name ) { + const fontFamilies = useSetting( 'typography.fontFamilies' ); + const fontSizes = useSetting( 'typography.fontSizes' ); + const customFontSize = useSetting( 'typography.customFontSize' ); + const fontStyle = useSetting( 'typography.fontStyle' ); + const fontWeight = useSetting( 'typography.fontWeight' ); + const lineHeight = useSetting( 'typography.lineHeight' ); + const textDecoration = useSetting( 'typography.textDecoration' ); + const textTransform = useSetting( 'typography.textTransform' ); + const letterSpacing = useSetting( 'typography.letterSpacing' ); + const supports = useSupportedStyles( name, null ); + + return useMemo( () => { + const rawSettings = { + typography: { + fontFamilies: { + custom: fontFamilies, + }, + fontSizes: { + custom: fontSizes, + }, + customFontSize, + fontStyle, + fontWeight, + lineHeight, + textDecoration, + textTransform, + letterSpacing, + }, + }; + return overrideSettingsWithSupports( rawSettings, supports ); + }, [ + fontFamilies, + fontSizes, + customFontSize, + fontStyle, + fontWeight, + lineHeight, + textDecoration, + textTransform, + letterSpacing, + supports, + ] ); +} + export function TypographyPanel( { clientId, name, attributes, setAttributes, } ) { - const settings = { - typography: { - fontFamilies: { - custom: useSetting( 'typography.fontFamilies' ), - }, - fontSizes: { - custom: useSetting( 'typography.fontSizes' ), - }, - customFontSize: useSetting( 'typography.customFontSize' ), - fontStyle: useSetting( 'typography.fontStyle' ), - fontWeight: useSetting( 'typography.fontWeight' ), - lineHeight: useSetting( 'typography.lineHeight' ), - textDecoration: useSetting( 'typography.textDecoration' ), - textTransform: useSetting( 'typography.textTransform' ), - letterSpacing: useSetting( 'typography.letterSpacing' ), - }, - }; - - const isSupported = hasTypographySupport( name ); - const isEnabled = useHasTypographyPanel( name, null, settings ); - + const settings = useBlockSettings( name ); + const isEnabled = useHasTypographyPanel( settings ); const value = useMemo( () => { return { ...attributes.style, @@ -115,7 +145,7 @@ export function TypographyPanel( { } ); }; - if ( ! isEnabled || ! isSupported ) { + if ( ! isEnabled ) { return null; } diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js index 82ef4f30e5395..e272043c7ebab 100644 --- a/packages/block-editor/src/index.js +++ b/packages/block-editor/src/index.js @@ -20,4 +20,4 @@ export * from './elements'; export * from './utils'; export { storeConfig, store } from './store'; export { SETTINGS_DEFAULTS } from './store/defaults'; -export { experiments } from './private-apis'; +export { privateApis } from './private-apis'; diff --git a/packages/block-editor/src/layouts/flow.js b/packages/block-editor/src/layouts/flow.js index 9367d5b91508a..d064edce65fed 100644 --- a/packages/block-editor/src/layouts/flow.js +++ b/packages/block-editor/src/layouts/flow.js @@ -56,7 +56,7 @@ export default { getOrientation() { return 'vertical'; }, - getAlignments( layout ) { + getAlignments( layout, isBlockBasedTheme ) { const alignmentInfo = getAlignmentsInfo( layout ); if ( layout.alignments !== undefined ) { if ( ! layout.alignments.includes( 'none' ) ) { @@ -74,6 +74,21 @@ export default { { name: 'right' }, ]; + // This is for backwards compatibility with hybrid themes. + if ( ! isBlockBasedTheme ) { + const { contentSize, wideSize } = layout; + if ( contentSize ) { + alignments.unshift( { name: 'full' } ); + } + + if ( wideSize ) { + alignments.unshift( { + name: 'wide', + info: alignmentInfo.wide, + } ); + } + } + alignments.unshift( { name: 'none', info: alignmentInfo.none } ); return alignments; diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index a624480bbcce1..b5b6c5d934cd4 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -8,10 +8,10 @@ import OffCanvasEditor from './components/off-canvas-editor'; import LeafMoreMenu from './components/off-canvas-editor/leaf-more-menu'; /** - * Experimental @wordpress/block-editor APIs. + * Private @wordpress/block-editor APIs. */ -export const experiments = {}; -lock( experiments, { +export const privateApis = {}; +lock( privateApis, { ...globalStyles, ExperimentalBlockEditorProvider, LeafMoreMenu, diff --git a/packages/block-editor/src/private-apis.native.js b/packages/block-editor/src/private-apis.native.js index 39400dbe10593..5555e00477e7b 100644 --- a/packages/block-editor/src/private-apis.native.js +++ b/packages/block-editor/src/private-apis.native.js @@ -6,10 +6,10 @@ import { ExperimentalBlockEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; /** - * Experimental @wordpress/block-editor APIs. + * Private @wordpress/block-editor APIs. */ -export const experiments = {}; -lock( experiments, { +export const privateApis = {}; +lock( privateApis, { ...globalStyles, ExperimentalBlockEditorProvider, } ); diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index f2d41a42e6dc2..a8d7caaba6e0c 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -16,30 +16,35 @@ "type": "string", "source": "attribute", "selector": "a", - "attribute": "href" + "attribute": "href", + "__experimentalRole": "content" }, "title": { "type": "string", "source": "attribute", "selector": "a", - "attribute": "title" + "attribute": "title", + "__experimentalRole": "content" }, "text": { "type": "string", "source": "html", - "selector": "a" + "selector": "a", + "__experimentalRole": "content" }, "linkTarget": { "type": "string", "source": "attribute", "selector": "a", - "attribute": "target" + "attribute": "target", + "__experimentalRole": "content" }, "rel": { "type": "string", "source": "attribute", "selector": "a", - "attribute": "rel" + "attribute": "rel", + "__experimentalRole": "content" }, "placeholder": { "type": "string" diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index edb66416af88c..0b9e3e451337e 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -93,3 +93,8 @@ * These are only output in the editor, but styles here are NOT prefixed .editor-styles-wrapper. * This allows us to create normalization styles that are easily overridden by editor styles. */ + +// Remove the browser default border for iframe in Custom HTML block, Embed block, etc. +.editor-styles-wrapper iframe:not([frameborder]) { + border: 0; +} diff --git a/packages/block-library/src/latest-comments/block.json b/packages/block-library/src/latest-comments/block.json index b7ba5c71a6832..80fa4f5d2d063 100644 --- a/packages/block-library/src/latest-comments/block.json +++ b/packages/block-library/src/latest-comments/block.json @@ -34,6 +34,19 @@ "spacing": { "margin": true, "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } } }, "editorStyle": "wp-block-latest-comments-editor", diff --git a/packages/block-library/src/latest-comments/edit.js b/packages/block-library/src/latest-comments/edit.js index 01eb9e11dc3de..5675c07df513e 100644 --- a/packages/block-library/src/latest-comments/edit.js +++ b/packages/block-library/src/latest-comments/edit.js @@ -28,6 +28,14 @@ export default function LatestComments( { attributes, setAttributes } ) { const { commentsToShow, displayAvatar, displayDate, displayExcerpt } = attributes; + const serverSideAttributes = { + ...attributes, + style: { + ...attributes?.style, + spacing: undefined, + }, + }; + return (
@@ -71,8 +79,7 @@ export default function LatestComments( { attributes, setAttributes } ) { ( diff --git a/packages/block-library/src/navigation/edit/menu-inspector-controls.js b/packages/block-library/src/navigation/edit/menu-inspector-controls.js index 26bf21168ee88..9b6c1456a3726 100644 --- a/packages/block-library/src/navigation/edit/menu-inspector-controls.js +++ b/packages/block-library/src/navigation/edit/menu-inspector-controls.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { - experiments as blockEditorExperiments, + privateApis as blockEditorPrivateApis, InspectorControls, store as blockEditorStore, } from '@wordpress/block-editor'; @@ -33,7 +33,7 @@ const MainContent = ( { isNavigationMenuMissing, onCreateNew, } ) => { - const { OffCanvasEditor, LeafMoreMenu } = unlock( blockEditorExperiments ); + const { OffCanvasEditor, LeafMoreMenu } = unlock( blockEditorPrivateApis ); // Provide a hierarchy of clientIds for the given Navigation block (clientId). // This is required else the list view will display the entire block tree. const clientIdsTree = useSelect( diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index d29270a35109d..03aaae31c5bc9 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -373,12 +373,14 @@ function block_core_navigation_get_most_recently_published_navigation() { // Default to the most recently created menu. $parsed_args = array( - 'post_type' => 'wp_navigation', - 'no_found_rows' => true, - 'order' => 'DESC', - 'orderby' => 'date', - 'post_status' => 'publish', - 'posts_per_page' => 1, // get only the most recent. + 'post_type' => 'wp_navigation', + 'no_found_rows' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'order' => 'DESC', + 'orderby' => 'date', + 'post_status' => 'publish', + 'posts_per_page' => 1, // get only the most recent. ); $navigation_post = new WP_Query( $parsed_args ); diff --git a/packages/block-library/src/navigation/use-navigation-menu.js b/packages/block-library/src/navigation/use-navigation-menu.js index 7ce66683300a3..607a92cb82f95 100644 --- a/packages/block-library/src/navigation/use-navigation-menu.js +++ b/packages/block-library/src/navigation/use-navigation-menu.js @@ -24,7 +24,7 @@ export default function useNavigationMenu( ref ) { navigationMenus, isResolvingNavigationMenus, hasResolvedNavigationMenus, - } = selectNavigationMenus( select, ref ); + } = selectNavigationMenus( select ); const { navigationMenu, diff --git a/packages/block-library/src/page-list/index.php b/packages/block-library/src/page-list/index.php index efc73ad3a0a48..b9ef8bf745a73 100644 --- a/packages/block-library/src/page-list/index.php +++ b/packages/block-library/src/page-list/index.php @@ -150,7 +150,8 @@ function block_core_page_list_render_nested_page_list( $open_submenus_on_click, if ( empty( $nested_pages ) ) { return; } - $markup = ''; + $front_page_id = (int) get_option( 'page_on_front' ); + $markup = ''; foreach ( (array) $nested_pages as $page ) { $css_class = $page['is_active'] ? ' current-menu-item' : ''; $aria_current = $page['is_active'] ? ' aria-current="page"' : ''; @@ -181,7 +182,6 @@ function block_core_page_list_render_nested_page_list( $open_submenus_on_click, } } - $front_page_id = (int) get_option( 'page_on_front' ); if ( (int) $page['page_id'] === $front_page_id ) { $css_class .= ' menu-item-home'; } @@ -282,14 +282,14 @@ function render_block_core_page_list( $attributes, $content, $block ) { $pages_with_children[ $page->post_parent ][ $page->ID ] = array( 'page_id' => $page->ID, 'title' => $page->post_title, - 'link' => get_permalink( $page->ID ), + 'link' => get_permalink( $page ), 'is_active' => $is_active, ); } else { $top_level_pages[ $page->ID ] = array( 'page_id' => $page->ID, 'title' => $page->post_title, - 'link' => get_permalink( $page->ID ), + 'link' => get_permalink( $page ), 'is_active' => $is_active, ); diff --git a/packages/block-library/src/post-title/index.php b/packages/block-library/src/post-title/index.php index 1f03b12fc2b46..e123a9993304b 100644 --- a/packages/block-library/src/post-title/index.php +++ b/packages/block-library/src/post-title/index.php @@ -19,8 +19,8 @@ function render_block_core_post_title( $attributes, $content, $block ) { return ''; } - $post_ID = $block->context['postId']; - $title = get_the_title(); + $post = get_post( $block->context['postId'] ); + $title = get_the_title( $post ); if ( ! $title ) { return ''; @@ -33,7 +33,7 @@ function render_block_core_post_title( $attributes, $content, $block ) { if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) { $rel = ! empty( $attributes['rel'] ) ? 'rel="' . esc_attr( $attributes['rel'] ) . '"' : ''; - $title = sprintf( '%4$s', get_the_permalink( $post_ID ), esc_attr( $attributes['linkTarget'] ), $rel, $title ); + $title = sprintf( '%4$s', get_the_permalink( $post ), esc_attr( $attributes['linkTarget'] ), $rel, $title ); } $classes = array(); diff --git a/packages/block-library/src/quote/style.scss b/packages/block-library/src/quote/style.scss index 835dc56afaf5e..e453e407a0875 100644 --- a/packages/block-library/src/quote/style.scss +++ b/packages/block-library/src/quote/style.scss @@ -2,8 +2,8 @@ box-sizing: border-box; overflow-wrap: break-word; // Break long strings of text without spaces so they don't overflow the block. // .is-style-large and .is-large are kept for backwards compatibility. The :not pseudo-class is used to enable switching styles. See PR #37580. - &.is-style-large:not(.is-style-plain), - &.is-large:not(.is-style-plain) { + &.is-style-large:where(:not(.is-style-plain)), + &.is-large:where(:not(.is-style-plain)) { margin-bottom: 1em; padding: 0 1em; diff --git a/packages/block-library/src/template-part/index.php b/packages/block-library/src/template-part/index.php index 7c4cb693fde54..b0507e46464a2 100644 --- a/packages/block-library/src/template-part/index.php +++ b/packages/block-library/src/template-part/index.php @@ -22,7 +22,7 @@ function render_block_core_template_part( $attributes ) { if ( isset( $attributes['slug'] ) && isset( $attributes['theme'] ) && - wp_get_theme()->get_stylesheet() === $attributes['theme'] + get_stylesheet() === $attributes['theme'] ) { $template_part_id = $attributes['theme'] . '//' . $attributes['slug']; $template_part_query = new WP_Query( diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index dc0772cc9aa52..4f023d716b5c5 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -47,6 +47,12 @@ "spacing": { "margin": true, "padding": true + }, + "__experimentalBorder": { + "radius": true, + "width": true, + "color": true, + "style": true } }, "style": "wp-block-verse", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index cacd30b6d28af..8889af90dd997 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,11 +2,17 @@ ## Unreleased +### Bug Fix + +- `ToolsPanel`: fix type inconsistencies between types, docs and normal component usage ([47944](https://github.com/WordPress/gutenberg/pull/47944)). +- `SelectControl`: Fix styling when `multiple` prop is enabled ([#47893](https://github.com/WordPress/gutenberg/pull/43213)). + ### Enhancements - `ColorPalette`, `GradientPicker`, `PaletteEdit`, `ToolsPanel`: add new props to set a custom heading level ([43848](https://github.com/WordPress/gutenberg/pull/43848) and [#47788](https://github.com/WordPress/gutenberg/pull/47788)). - `ColorPalette`: ensure text label contrast checking works with CSS variables ([#47373](https://github.com/WordPress/gutenberg/pull/47373)). - `Navigator`: Support dynamic paths with parameters ([#47827](https://github.com/WordPress/gutenberg/pull/47827)). +- `Navigator`: Support hierarchical paths navigation and add `NavigatorToParentButton` component ([#47883](https://github.com/WordPress/gutenberg/pull/47883)). ### Internal @@ -24,6 +30,7 @@ - `ToolsPanel`: Allow display of optional items when values are updated externally to item controls ([47727](https://github.com/WordPress/gutenberg/pull/47727)). - `ToolsPanel`: Ensure display of optional items when values are updated externally and multiple blocks selected ([47864](https://github.com/WordPress/gutenberg/pull/47864)). - `Navigator`: add more pattern matching tests, refine existing tests ([47910](https://github.com/WordPress/gutenberg/pull/47910)). +- `ToolsPanel`: Refactor Storybook examples to TypeScript ([47944](https://github.com/WordPress/gutenberg/pull/47944)). ## 23.3.0 (2023-02-01) diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap index 56e9a4ffffe6d..8edf2a4875e52 100644 --- a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap +++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap @@ -116,7 +116,6 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` width: 100%; max-width: none; cursor: pointer; - overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 16px; @@ -126,6 +125,7 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` padding-bottom: 0; padding-left: 8px; padding-right: 26px; + overflow: hidden; } @media ( min-width: 600px ) { @@ -387,7 +387,6 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` width: 100%; max-width: none; cursor: pointer; - overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 16px; @@ -397,6 +396,7 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` padding-bottom: 0; padding-left: 8px; padding-right: 26px; + overflow: hidden; } @media ( min-width: 600px ) { @@ -668,7 +668,6 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] width: 100%; max-width: none; cursor: pointer; - overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 16px; @@ -678,6 +677,7 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] padding-bottom: 0; padding-left: 8px; padding-right: 26px; + overflow: hidden; } @media ( min-width: 600px ) { @@ -961,7 +961,6 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] width: 100%; max-width: none; cursor: pointer; - overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 16px; @@ -971,6 +970,7 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] padding-bottom: 0; padding-left: 8px; padding-right: 26px; + overflow: hidden; } @media ( min-width: 600px ) { diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 203469ccb1be3..90ed86944dbec 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -115,6 +115,7 @@ export { NavigatorScreen as __experimentalNavigatorScreen, NavigatorButton as __experimentalNavigatorButton, NavigatorBackButton as __experimentalNavigatorBackButton, + NavigatorToParentButton as __experimentalNavigatorToParentButton, useNavigator as __experimentalUseNavigator, } from './navigator'; export { default as Notice } from './notice'; @@ -212,5 +213,5 @@ export { export { default as withNotices } from './higher-order/with-notices'; export { default as withSpokenMessages } from './higher-order/with-spoken-messages'; -// Experiments. -export { experiments } from './private-apis'; +// Private APIs. +export { privateApis } from './private-apis'; diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index ed00dd2624452..d9c7b602b8392 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -14,6 +14,7 @@ import { useRef, useState, forwardRef, + useLayoutEffect, } from '@wordpress/element'; import { useInstanceId, @@ -25,6 +26,7 @@ import { } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { close } from '@wordpress/icons'; +import { getScrollContainer } from '@wordpress/dom'; /** * Internal dependencies @@ -76,8 +78,26 @@ function UnforwardedModal( const constrainedTabbingRef = useConstrainedTabbing(); const focusReturnRef = useFocusReturn(); const focusOutsideProps = useFocusOutside( onRequestClose ); + const contentRef = useRef< HTMLDivElement >( null ); + const childrenContainerRef = useRef< HTMLDivElement >( null ); const [ hasScrolledContent, setHasScrolledContent ] = useState( false ); + const [ hasScrollableContent, setHasScrollableContent ] = useState( false ); + + // Determines whether the Modal content is scrollable and updates the state. + const isContentScrollable = useCallback( () => { + if ( ! contentRef.current ) { + return; + } + + const closestScrollContainer = getScrollContainer( contentRef.current ); + + if ( contentRef.current === closestScrollContainer ) { + setHasScrollableContent( true ); + } else { + setHasScrollableContent( false ); + } + }, [ contentRef ] ); useEffect( () => { openModalCount++; @@ -97,6 +117,22 @@ function UnforwardedModal( }; }, [ bodyOpenClassName ] ); + // Calls the isContentScrollable callback when the Modal children container resizes. + useLayoutEffect( () => { + if ( ! window.ResizeObserver || ! childrenContainerRef.current ) { + return; + } + + const resizeObserver = new ResizeObserver( isContentScrollable ); + resizeObserver.observe( childrenContainerRef.current ); + + isContentScrollable(); + + return () => { + resizeObserver.disconnect(); + }; + }, [ isContentScrollable, childrenContainerRef ] ); + function handleEscapeKeyDown( event: KeyboardEvent< HTMLDivElement > ) { if ( // Ignore keydowns from IMEs @@ -172,10 +208,18 @@ function UnforwardedModal(
{ ! __experimentalHideHeader && (
@@ -208,7 +252,7 @@ function UnforwardedModal( ) }
) } - { children } +
{ children }
diff --git a/packages/components/src/modal/style.scss b/packages/components/src/modal/style.scss index 2374a7e57993d..f434147ecef62 100644 --- a/packages/components/src/modal/style.scss +++ b/packages/components/src/modal/style.scss @@ -129,4 +129,12 @@ margin-top: 0; padding-top: $grid-unit-30; } + + &.is-scrollable:focus-visible { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + outline-offset: -2px; + } } diff --git a/packages/components/src/modal/types.ts b/packages/components/src/modal/types.ts index 45fe0580d5329..6169e42a8a2d4 100644 --- a/packages/components/src/modal/types.ts +++ b/packages/components/src/modal/types.ts @@ -50,6 +50,8 @@ export type ModalProps = { className?: string; /** * Label on the close button. + * + * @default `__( 'Close' )` */ closeButtonLabel?: string; /** diff --git a/packages/components/src/navigator/context.ts b/packages/components/src/navigator/context.ts index fd069b616a7f8..e195621b03d20 100644 --- a/packages/components/src/navigator/context.ts +++ b/packages/components/src/navigator/context.ts @@ -12,6 +12,7 @@ const initialContextValue: NavigatorContextType = { location: {}, goTo: () => {}, goBack: () => {}, + goToParent: () => {}, addScreen: () => {}, removeScreen: () => {}, params: {}, diff --git a/packages/components/src/navigator/index.ts b/packages/components/src/navigator/index.ts index 49f5655dc4b39..74c69a0daa9c3 100644 --- a/packages/components/src/navigator/index.ts +++ b/packages/components/src/navigator/index.ts @@ -2,4 +2,5 @@ export { NavigatorProvider } from './navigator-provider'; export { NavigatorScreen } from './navigator-screen'; export { NavigatorButton } from './navigator-button'; export { NavigatorBackButton } from './navigator-back-button'; +export { NavigatorToParentButton } from './navigator-to-parent-button'; export { default as useNavigator } from './use-navigator'; diff --git a/packages/components/src/navigator/navigator-back-button/README.md b/packages/components/src/navigator/navigator-back-button/README.md index d147027697597..01d4221be536e 100644 --- a/packages/components/src/navigator/navigator-back-button/README.md +++ b/packages/components/src/navigator/navigator-back-button/README.md @@ -10,22 +10,6 @@ The `NavigatorBackButton` component can be used to navigate to a screen and shou Refer to [the `NavigatorProvider` component](/packages/components/src/navigator/navigator-provider/README.md#usage) for a usage example. -## Props - -The component accepts the following props: - -### `onClick`: `React.MouseEventHandler< HTMLElement >` - -The callback called in response to a `click` event. - -- Required: No - -### `path`: `string` - -The path of the screen to navigate to. - -- Required: Yes - ### Inherited props -`NavigatorBackButton` also inherits all of the [`Button` props](/packages/components/src/button/README.md#props), except for `href`. +`NavigatorBackButton` also inherits all of the [`Button` props](/packages/components/src/button/README.md#props), except for `href` and `target`. diff --git a/packages/components/src/navigator/navigator-back-button/hook.ts b/packages/components/src/navigator/navigator-back-button/hook.ts index 5e7adabf3d9bb..437c60731cc95 100644 --- a/packages/components/src/navigator/navigator-back-button/hook.ts +++ b/packages/components/src/navigator/navigator-back-button/hook.ts @@ -9,26 +9,31 @@ import { useCallback } from '@wordpress/element'; import { useContextSystem, WordPressComponentProps } from '../../ui/context'; import Button from '../../button'; import useNavigator from '../use-navigator'; -import type { NavigatorBackButtonProps } from '../types'; +import type { NavigatorBackButtonHookProps } from '../types'; export function useNavigatorBackButton( - props: WordPressComponentProps< NavigatorBackButtonProps, 'button' > + props: WordPressComponentProps< NavigatorBackButtonHookProps, 'button' > ) { const { onClick, as = Button, + goToParent: goToParentProp = false, ...otherProps } = useContextSystem( props, 'NavigatorBackButton' ); - const { goBack } = useNavigator(); + const { goBack, goToParent } = useNavigator(); const handleClick: React.MouseEventHandler< HTMLButtonElement > = useCallback( ( e ) => { e.preventDefault(); - goBack(); + if ( goToParentProp ) { + goToParent(); + } else { + goBack(); + } onClick?.( e ); }, - [ goBack, onClick ] + [ goToParentProp, goToParent, goBack, onClick ] ); return { diff --git a/packages/components/src/navigator/navigator-provider/README.md b/packages/components/src/navigator/navigator-provider/README.md index 6f90cf31198e9..8be27a6510184 100644 --- a/packages/components/src/navigator/navigator-provider/README.md +++ b/packages/components/src/navigator/navigator-provider/README.md @@ -4,7 +4,7 @@ This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
-The `NavigatorProvider` component allows rendering nested views/panels/menus (via the [`NavigatorScreen` component](/packages/components/src/navigator/navigator-screen/README.md)) and navigate between these different states (via the [`NavigatorButton`](/packages/components/src/navigator/navigator-button/README.md) and [`NavigatorBackButton`](/packages/components/src/navigator/navigator-back-button/README.md) components or the `useNavigator` hook). The Global Styles sidebar is an example of this. +The `NavigatorProvider` component allows rendering nested views/panels/menus (via the [`NavigatorScreen` component](/packages/components/src/navigator/navigator-screen/README.md)) and navigate between these different states (via the [`NavigatorButton`](/packages/components/src/navigator/navigator-button/README.md), [`NavigatorToParentButton`](/packages/components/src/navigator/navigator-to-parent-button/README.md) and [`NavigatorBackButton`](/packages/components/src/navigator/navigator-back-button/README.md) components or the `useNavigator` hook). The Global Styles sidebar is an example of this. ## Usage @@ -13,7 +13,7 @@ import { __experimentalNavigatorProvider as NavigatorProvider, __experimentalNavigatorScreen as NavigatorScreen, __experimentalNavigatorButton as NavigatorButton, - __experimentalNavigatorBackButton as NavigatorBackButton, + __experimentalNavigatorToParentButton as NavigatorToParentButton, } from '@wordpress/components'; const MyNavigation = () => ( @@ -27,13 +27,21 @@ const MyNavigation = () => (

This is the child screen.

- + Go back - +
); ``` +**Important note** + +Parent/child navigation only works if the path you define are hierarchical, following a URL-like scheme where each path segment is separated by the `/` character. +For example: +- `/` is the root of all paths. There should always be a screen with `path="/"`. +- `/parent/child` is a child of `/parent`. +- `/parent/child/grand-child` is a child of `/parent/child`. +- `/parent/:param` is a child of `/parent` as well. ## Props @@ -58,6 +66,15 @@ The `goTo` function allows navigating to a given path. The second argument can a The available options are: - `focusTargetSelector`: `string`. An optional property used to specify the CSS selector used to restore focus on the matching element when navigating back. +- `isBack`: `boolean`. An optional property used to specify whether the navigation should be considered as backwards (thus enabling focus restoration when possible, and causing the animation to be backwards too) + +### `goToParent`: `() => void;` + +The `goToParent` function allows navigating to the parent screen. + +Parent/child navigation only works if the path you define are hierarchical (see note above). + +When a match is not found, the function will try to recursively navigate the path hierarchy until a matching screen (or the root `/`) are found. ### `goBack`: `() => void` diff --git a/packages/components/src/navigator/navigator-provider/component.tsx b/packages/components/src/navigator/navigator-provider/component.tsx index 77447d6e97fca..28e710fa577b2 100644 --- a/packages/components/src/navigator/navigator-provider/component.tsx +++ b/packages/components/src/navigator/navigator-provider/component.tsx @@ -13,6 +13,7 @@ import { useCallback, useReducer, useRef, + useEffect, } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; @@ -33,11 +34,13 @@ import type { NavigatorContext as NavigatorContextType, Screen, } from '../types'; -import { patternMatch } from '../utils/router'; +import { patternMatch, findParent } from '../utils/router'; type MatchedPath = ReturnType< typeof patternMatch >; type ScreenAction = { type: string; screen: Screen }; +const MAX_HISTORY_LENGTH = 50; + function screensReducer( state: Screen[] = [], action: ScreenAction @@ -66,7 +69,15 @@ function UnconnectedNavigatorProvider( path: initialPath, }, ] ); + const currentLocationHistory = useRef< NavigatorLocation[] >( [] ); const [ screens, dispatch ] = useReducer( screensReducer, [] ); + const currentScreens = useRef< Screen[] >( [] ); + useEffect( () => { + currentScreens.current = screens; + }, [ screens ] ); + useEffect( () => { + currentLocationHistory.current = locationHistory; + }, [ locationHistory ] ); const currentMatch = useRef< MatchedPath >(); const matchedPath = useMemo( () => { let currentPath: string | undefined; @@ -115,15 +126,47 @@ function UnconnectedNavigatorProvider( [] ); + const goBack: NavigatorContextType[ 'goBack' ] = useCallback( () => { + setLocationHistory( ( prevLocationHistory ) => { + if ( prevLocationHistory.length <= 1 ) { + return prevLocationHistory; + } + return [ + ...prevLocationHistory.slice( 0, -2 ), + { + ...prevLocationHistory[ prevLocationHistory.length - 2 ], + isBack: true, + hasRestoredFocus: false, + }, + ]; + } ); + }, [] ); + const goTo: NavigatorContextType[ 'goTo' ] = useCallback( ( path, options = {} ) => { - setLocationHistory( ( prevLocationHistory ) => { - const { focusTargetSelector, ...restOptions } = options; + const { + focusTargetSelector, + isBack = false, + ...restOptions + } = options; + + const isNavigatingToPreviousPath = + isBack && + currentLocationHistory.current.length > 1 && + currentLocationHistory.current[ + currentLocationHistory.current.length - 2 + ].path === path; + + if ( isNavigatingToPreviousPath ) { + goBack(); + return; + } + setLocationHistory( ( prevLocationHistory ) => { const newLocation = { ...restOptions, path, - isBack: false, + isBack, hasRestoredFocus: false, }; @@ -132,7 +175,12 @@ function UnconnectedNavigatorProvider( } return [ - ...prevLocationHistory.slice( 0, -1 ), + ...prevLocationHistory.slice( + prevLocationHistory.length > MAX_HISTORY_LENGTH - 1 + ? 1 + : 0, + -1 + ), // Assign `focusTargetSelector` to the previous location in history // (the one we just navigated from). { @@ -145,24 +193,27 @@ function UnconnectedNavigatorProvider( ]; } ); }, - [] + [ goBack ] ); - const goBack: NavigatorContextType[ 'goBack' ] = useCallback( () => { - setLocationHistory( ( prevLocationHistory ) => { - if ( prevLocationHistory.length <= 1 ) { - return prevLocationHistory; + const goToParent: NavigatorContextType[ 'goToParent' ] = + useCallback( () => { + const currentPath = + currentLocationHistory.current[ + currentLocationHistory.current.length - 1 + ].path; + if ( currentPath === undefined ) { + return; } - return [ - ...prevLocationHistory.slice( 0, -2 ), - { - ...prevLocationHistory[ prevLocationHistory.length - 2 ], - isBack: true, - hasRestoredFocus: false, - }, - ]; - } ); - }, [] ); + const parentPath = findParent( + currentPath, + currentScreens.current + ); + if ( parentPath === undefined ) { + return; + } + goTo( parentPath, { isBack: true } ); + }, [ goTo ] ); const navigatorContextValue: NavigatorContextType = useMemo( () => ( { @@ -174,10 +225,19 @@ function UnconnectedNavigatorProvider( match: matchedPath ? matchedPath.id : undefined, goTo, goBack, + goToParent, addScreen, removeScreen, } ), - [ locationHistory, matchedPath, goTo, goBack, addScreen, removeScreen ] + [ + locationHistory, + matchedPath, + goTo, + goBack, + goToParent, + addScreen, + removeScreen, + ] ); const cx = useCx(); diff --git a/packages/components/src/navigator/navigator-to-parent-button/README.md b/packages/components/src/navigator/navigator-to-parent-button/README.md new file mode 100644 index 0000000000000..62dacc3dfa4ea --- /dev/null +++ b/packages/components/src/navigator/navigator-to-parent-button/README.md @@ -0,0 +1,15 @@ +# `NavigatorToParentButton` + +
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +
+ +The `NavigatorToParentButton` component can be used to navigate to a screen and should be used in combination with the [`NavigatorProvider`](/packages/components/src/navigator/navigator-provider/README.md), the [`NavigatorScreen`](/packages/components/src/navigator/navigator-screen/README.md) and the [`NavigatorButton`](/packages/components/src/navigator/navigator-button/README.md) components (or the `useNavigator` hook). + +## Usage + +Refer to [the `NavigatorProvider` component](/packages/components/src/navigator/navigator-provider/README.md#usage) for a usage example. + +### Inherited props + +`NavigatorToParentButton` also inherits all of the [`Button` props](/packages/components/src/button/README.md#props), except for `href` and `target`. diff --git a/packages/components/src/navigator/navigator-to-parent-button/component.tsx b/packages/components/src/navigator/navigator-to-parent-button/component.tsx new file mode 100644 index 0000000000000..5dd8ab1624ae9 --- /dev/null +++ b/packages/components/src/navigator/navigator-to-parent-button/component.tsx @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import type { ForwardedRef } from 'react'; + +/** + * Internal dependencies + */ +import { contextConnect, WordPressComponentProps } from '../../ui/context'; +import { View } from '../../view'; +import { useNavigatorBackButton } from '../navigator-back-button/hook'; +import type { NavigatorToParentButtonProps } from '../types'; + +function UnconnectedNavigatorToParentButton( + props: WordPressComponentProps< NavigatorToParentButtonProps, 'button' >, + forwardedRef: ForwardedRef< any > +) { + const navigatorToParentButtonProps = useNavigatorBackButton( { + ...props, + goToParent: true, + } ); + + return ; +} + +/* + * The `NavigatorToParentButton` component can be used to navigate to a screen and + * should be used in combination with the `NavigatorProvider`, the + * `NavigatorScreen` and the `NavigatorButton` components (or the `useNavigator` + * hook). + * + * @example + * ```jsx + * import { + * __experimentalNavigatorProvider as NavigatorProvider, + * __experimentalNavigatorScreen as NavigatorScreen, + * __experimentalNavigatorButton as NavigatorButton, + * __experimentalNavigatorToParentButton as NavigatorToParentButton, + * } from '@wordpress/components'; + * + * const MyNavigation = () => ( + * + * + *

This is the home screen.

+ * + * Navigate to child screen. + * + *
+ * + * + *

This is the child screen.

+ * + * Go to parent + * + *
+ *
+ * ); + * ``` + */ +export const NavigatorToParentButton = contextConnect( + UnconnectedNavigatorToParentButton, + 'NavigatorToParentButton' +); + +export default NavigatorToParentButton; diff --git a/packages/components/src/navigator/navigator-to-parent-button/index.ts b/packages/components/src/navigator/navigator-to-parent-button/index.ts new file mode 100644 index 0000000000000..f5218e456065e --- /dev/null +++ b/packages/components/src/navigator/navigator-to-parent-button/index.ts @@ -0,0 +1 @@ +export { default as NavigatorToParentButton } from './component'; diff --git a/packages/components/src/navigator/stories/index.tsx b/packages/components/src/navigator/stories/index.tsx index ffc7a87ae1cc1..4c8a968e881f1 100644 --- a/packages/components/src/navigator/stories/index.tsx +++ b/packages/components/src/navigator/stories/index.tsx @@ -15,6 +15,7 @@ import { NavigatorScreen, NavigatorButton, NavigatorBackButton, + NavigatorToParentButton, useNavigator, } from '..'; @@ -232,3 +233,68 @@ function ProductDetails() { ); } + +const NestedNavigatorTemplate: ComponentStory< typeof NavigatorProvider > = ( { + style, + ...props +} ) => ( + + + + + + Go to first child. + + + Go to second child. + + + + + + + + This is the first child + + Go back to parent + + + + + + + + This is the second child + + Go back to parent + + + Go to grand child. + + + + + + + + This is the grand child + + Go back to parent + + + + + +); + +export const NestedNavigator: ComponentStory< typeof NavigatorProvider > = + NestedNavigatorTemplate.bind( {} ); +NestedNavigator.args = { + initialPath: '/child2/grandchild', +}; diff --git a/packages/components/src/navigator/test/index.tsx b/packages/components/src/navigator/test/index.tsx index dbc2ba30ebdb3..927f6f3abf0e0 100644 --- a/packages/components/src/navigator/test/index.tsx +++ b/packages/components/src/navigator/test/index.tsx @@ -13,11 +13,13 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ +import Button from '../../button'; import { NavigatorProvider, NavigatorScreen, NavigatorButton, NavigatorBackButton, + NavigatorToParentButton, useNavigator, } from '..'; @@ -75,6 +77,7 @@ const BUTTON_TEXT = { toInvalidHtmlPathScreen: 'Navigate to screen with an invalid HTML value as a path.', back: 'Go back', + backUsingGoTo: 'Go back using goTo', }; type CustomTestOnClickHandler = ( @@ -84,6 +87,7 @@ type CustomTestOnClickHandler = ( path: string; } | { type: 'goBack' } + | { type: 'goToParent' } ) => void; function CustomNavigatorButton( { @@ -105,6 +109,26 @@ function CustomNavigatorButton( { ); } +function CustomNavigatorGoToBackButton( { + path, + onClick, + ...props +}: Omit< ComponentPropsWithoutRef< typeof NavigatorButton >, 'onClick' > & { + onClick?: CustomTestOnClickHandler; +} ) { + const { goTo } = useNavigator(); + return ( + + + { BUTTON_TEXT.toChildScreen } + + + + +

{ SCREEN_TEXT.child }

+ { /* + * A button useful to test focus restoration. This button is the first + * tabbable item in the screen, but should not receive focus when + * navigating to screen as a result of a backwards navigation. + */ } + + + { BUTTON_TEXT.toNestedScreen } + + + { BUTTON_TEXT.back } + +
+ + +

{ SCREEN_TEXT.nested }

+ + { BUTTON_TEXT.back } + + + { BUTTON_TEXT.backUsingGoTo } + +
+ + + ); +}; + const getScreen = ( screenKey: keyof typeof SCREEN_TEXT ) => screen.getByText( SCREEN_TEXT[ screenKey ] ); const queryScreen = ( screenKey: keyof typeof SCREEN_TEXT ) => @@ -592,5 +699,43 @@ describe( 'Navigator', () => { getNavigationButton( 'toInvalidHtmlPathScreen' ) ).toHaveFocus(); } ); + + it( 'should restore focus while using goTo and goToParent', async () => { + const user = userEvent.setup(); + + render( ); + + expect( getScreen( 'home' ) ).toBeInTheDocument(); + + // Navigate to child screen. + await user.click( getNavigationButton( 'toChildScreen' ) ); + expect( getScreen( 'child' ) ).toBeInTheDocument(); + + // Navigate to nested screen. + await user.click( getNavigationButton( 'toNestedScreen' ) ); + expect( getScreen( 'nested' ) ).toBeInTheDocument(); + expect( getNavigationButton( 'back' ) ).toBeInTheDocument(); + + // Navigate back to child screen using the back button. + await user.click( getNavigationButton( 'back' ) ); + expect( getScreen( 'child' ) ).toBeInTheDocument(); + expect( getNavigationButton( 'toNestedScreen' ) ).toHaveFocus(); + + // Re navigate to nested screen. + await user.click( getNavigationButton( 'toNestedScreen' ) ); + expect( getScreen( 'nested' ) ).toBeInTheDocument(); + expect( + getNavigationButton( 'backUsingGoTo' ) + ).toBeInTheDocument(); + + // Navigate back to child screen using the go to button. + await user.click( getNavigationButton( 'backUsingGoTo' ) ); + expect( getScreen( 'child' ) ).toBeInTheDocument(); + expect( getNavigationButton( 'toNestedScreen' ) ).toHaveFocus(); + + // Navigate back to home screen. + await user.click( getNavigationButton( 'back' ) ); + expect( getNavigationButton( 'toChildScreen' ) ).toHaveFocus(); + } ); } ); } ); diff --git a/packages/components/src/navigator/test/router.ts b/packages/components/src/navigator/test/router.ts index 7c60b846a4ab1..50e05fed47bb7 100644 --- a/packages/components/src/navigator/test/router.ts +++ b/packages/components/src/navigator/test/router.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { patternMatch } from '../utils/router'; +import { patternMatch, findParent } from '../utils/router'; describe( 'patternMatch', () => { it( 'should return undefined if not pattern is matched', () => { @@ -48,3 +48,75 @@ describe( 'patternMatch', () => { } ); } ); } ); + +describe( 'findParent', () => { + it( 'should return undefined if no parent is found', () => { + const result = findParent( '/test', [ + { id: 'route', path: '/test' }, + ] ); + expect( result ).toBeUndefined(); + } ); + + it( 'should return the parent path', () => { + const result = findParent( '/test', [ + { id: 'route1', path: '/test' }, + { id: 'route2', path: '/' }, + ] ); + expect( result ).toEqual( '/' ); + } ); + + it( 'should return to another parent path', () => { + const result = findParent( '/test/123', [ + { id: 'route1', path: '/test/:id' }, + { id: 'route2', path: '/test' }, + ] ); + expect( result ).toEqual( '/test' ); + } ); + + it( 'should return the parent path with params', () => { + const result = findParent( '/test/123/456', [ + { id: 'route1', path: '/test/:id/:subId' }, + { id: 'route2', path: '/test/:id' }, + ] ); + expect( result ).toEqual( '/test/123' ); + } ); + + it( 'should return the parent path with optional params', () => { + const result = findParent( '/test/123', [ + { id: 'route', path: '/test/:id?' }, + ] ); + expect( result ).toEqual( '/test' ); + } ); + + it( 'should return the grand parent if no parent found', () => { + const result = findParent( '/test/123/456', [ + { id: 'route1', path: '/test/:id/:subId' }, + { id: 'route2', path: '/test' }, + ] ); + expect( result ).toEqual( '/test' ); + } ); + + it( 'should return the root when no grand parent found', () => { + const result = findParent( '/test/nested/path', [ + { id: 'route1', path: '/other-path' }, + { id: 'route2', path: '/yet-another-path' }, + { id: 'root', path: '/' }, + ] ); + expect( result ).toEqual( '/' ); + } ); + + it( 'should return undefined when no potential parent found', () => { + const result = findParent( '/test/nested/path', [ + { id: 'route1', path: '/other-path' }, + { id: 'route2', path: '/yet-another-path' }, + ] ); + expect( result ).toBeUndefined(); + } ); + + it( 'should return undefined for non supported paths', () => { + const result = findParent( 'this-is-a-path', [ + { id: 'route', path: '/' }, + ] ); + expect( result ).toBeUndefined(); + } ); +} ); diff --git a/packages/components/src/navigator/types.ts b/packages/components/src/navigator/types.ts index 98494095e0038..f6d8f5c22b0a5 100644 --- a/packages/components/src/navigator/types.ts +++ b/packages/components/src/navigator/types.ts @@ -12,11 +12,11 @@ export type MatchParams = Record< string, string | string[] >; type NavigateOptions = { focusTargetSelector?: string; + isBack?: boolean; }; export type NavigatorLocation = NavigateOptions & { isInitial?: boolean; - isBack?: boolean; path?: string; hasRestoredFocus?: boolean; }; @@ -27,6 +27,7 @@ export type Navigator = { params: MatchParams; goTo: ( path: string, options?: NavigateOptions ) => void; goBack: () => void; + goToParent: () => void; }; export type NavigatorContext = Navigator & { @@ -59,6 +60,17 @@ export type NavigatorScreenProps = { export type NavigatorBackButtonProps = ButtonAsButtonProps; +export type NavigatorBackButtonHookProps = NavigatorBackButtonProps & { + /** + * Whether we should navigate to the parent screen. + * + * @default 'false' + */ + goToParent?: boolean; +}; + +export type NavigatorToParentButtonProps = NavigatorBackButtonProps; + export type NavigatorButtonProps = NavigatorBackButtonProps & { /** * The path of the screen to navigate to. The value of this prop needs to be diff --git a/packages/components/src/navigator/use-navigator.ts b/packages/components/src/navigator/use-navigator.ts index bef5d37f5039f..4d917649374d5 100644 --- a/packages/components/src/navigator/use-navigator.ts +++ b/packages/components/src/navigator/use-navigator.ts @@ -13,12 +13,14 @@ import type { Navigator } from './types'; * Retrieves a `navigator` instance. */ function useNavigator(): Navigator { - const { location, params, goTo, goBack } = useContext( NavigatorContext ); + const { location, params, goTo, goBack, goToParent } = + useContext( NavigatorContext ); return { location, goTo, goBack, + goToParent, params, }; } diff --git a/packages/components/src/navigator/utils/router.ts b/packages/components/src/navigator/utils/router.ts index 5675c415c200f..6ff5be66661f9 100644 --- a/packages/components/src/navigator/utils/router.ts +++ b/packages/components/src/navigator/utils/router.ts @@ -8,12 +8,16 @@ import { match } from 'path-to-regexp'; */ import type { Screen, MatchParams } from '../types'; +function matchPath( path: string, pattern: string ) { + const matchingFunction = match< MatchParams >( pattern, { + decode: decodeURIComponent, + } ); + return matchingFunction( path ); +} + export function patternMatch( path: string, screens: Screen[] ) { for ( const screen of screens ) { - const matchingFunction = match< MatchParams >( screen.path, { - decode: decodeURIComponent, - } ); - const matched = matchingFunction( path ); + const matched = matchPath( path, screen.path ); if ( matched ) { return { params: matched.params, id: screen.id }; } @@ -21,3 +25,25 @@ export function patternMatch( path: string, screens: Screen[] ) { return undefined; } + +export function findParent( path: string, screens: Screen[] ) { + if ( ! path.startsWith( '/' ) ) { + return undefined; + } + const pathParts = path.split( '/' ); + let parentPath; + while ( pathParts.length > 1 && parentPath === undefined ) { + pathParts.pop(); + const potentialParentPath = + pathParts.join( '/' ) === '' ? '/' : pathParts.join( '/' ); + if ( + screens.find( ( screen ) => { + return matchPath( potentialParentPath, screen.path ) !== false; + } ) + ) { + parentPath = potentialParentPath; + } + } + + return parentPath; +} diff --git a/packages/components/src/private-apis.js b/packages/components/src/private-apis.js index 8798ea1078515..07efc6f6a039b 100644 --- a/packages/components/src/private-apis.js +++ b/packages/components/src/private-apis.js @@ -15,8 +15,8 @@ export const { lock, unlock } = '@wordpress/components' ); -export const experiments = {}; -lock( experiments, { +export const privateApis = {}; +lock( privateApis, { CustomSelectControl, __experimentalPopoverLegacyPositionToPlacement, } ); diff --git a/packages/components/src/select-control/README.md b/packages/components/src/select-control/README.md index 1d96f2ba92b76..dc6e884a5e12b 100644 --- a/packages/components/src/select-control/README.md +++ b/packages/components/src/select-control/README.md @@ -187,7 +187,9 @@ If this property is added, a help text will be generated using help property as #### multiple -If this property is added, multiple values can be selected. The value passed should be an array. +If this property is added, multiple values can be selected. The `value` passed should be an array. + +In most cases, it is preferable to use the `FormTokenField` or `CheckboxControl` components instead. - Type: `Boolean` - Required: No diff --git a/packages/components/src/select-control/index.tsx b/packages/components/src/select-control/index.tsx index 2abd647388cf2..75569dac80a16 100644 --- a/packages/components/src/select-control/index.tsx +++ b/packages/components/src/select-control/index.tsx @@ -101,7 +101,9 @@ function UnforwardedSelectControl( isFocused={ isFocused } label={ label } size={ size } - suffix={ suffix || } + suffix={ + suffix || ( ! multiple && ) + } prefix={ prefix } labelPosition={ labelPosition } __next36pxDefaultSize={ __next36pxDefaultSize } diff --git a/packages/components/src/select-control/style.scss b/packages/components/src/select-control/style.scss index b59fa2160b952..1558314af554f 100644 --- a/packages/components/src/select-control/style.scss +++ b/packages/components/src/select-control/style.scss @@ -1,16 +1,6 @@ .components-select-control__input { - background: $white; - height: 36px; - line-height: 36px; - margin: 1px; outline: 0; - width: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important; - - @include break-medium() { - height: 28px; - line-height: 28px; - } } @media (max-width: #{ ($break-medium) }) { diff --git a/packages/components/src/select-control/styles/select-control-styles.ts b/packages/components/src/select-control/styles/select-control-styles.ts index e0976c9f50769..2374015d747fa 100644 --- a/packages/components/src/select-control/styles/select-control-styles.ts +++ b/packages/components/src/select-control/styles/select-control-styles.ts @@ -13,7 +13,10 @@ import type { SelectControlProps } from '../types'; import InputControlSuffixWrapper from '../../input-control/input-suffix-wrapper'; interface SelectProps - extends Pick< SelectControlProps, '__next36pxDefaultSize' | 'disabled' > { + extends Pick< + SelectControlProps, + '__next36pxDefaultSize' | 'disabled' | 'multiple' + > { // Using `selectSize` instead of `size` to avoid a type conflict with the // `size` HTML attribute of the `select` element. selectSize?: SelectControlProps[ 'size' ]; @@ -50,8 +53,15 @@ const fontSizeStyles = ( { selectSize = 'default' }: SelectProps ) => { const sizeStyles = ( { __next36pxDefaultSize, + multiple, selectSize = 'default', }: SelectProps ) => { + if ( multiple ) { + // When `multiple`, just use the native browser styles + // without setting explicit height. + return; + } + const sizes = { default: { height: 36, @@ -91,33 +101,37 @@ export const chevronIconSize = 18; const sizePaddings = ( { __next36pxDefaultSize, + multiple, selectSize = 'default', }: SelectProps ) => { - const iconWidth = chevronIconSize; - - const sizes = { - default: { - paddingLeft: 16, - paddingRight: 16 + iconWidth, - }, - small: { - paddingLeft: 8, - paddingRight: 8 + iconWidth, - }, - '__unstable-large': { - paddingLeft: 16, - paddingRight: 16 + iconWidth, - }, + const padding = { + default: 16, + small: 8, + '__unstable-large': 16, }; if ( ! __next36pxDefaultSize ) { - sizes.default = { - paddingLeft: 8, - paddingRight: 8 + iconWidth, - }; + padding.default = 8; } - return rtl( sizes[ selectSize ] || sizes.default ); + const selectedPadding = padding[ selectSize ] || padding.default; + + return rtl( { + paddingLeft: selectedPadding, + paddingRight: selectedPadding + chevronIconSize, + ...( multiple + ? { + paddingTop: selectedPadding, + paddingBottom: selectedPadding, + } + : {} ), + } ); +}; + +const overflowStyles = ( { multiple }: SelectProps ) => { + return { + overflow: multiple ? 'auto' : 'hidden', + }; }; // TODO: Resolve need to use &&& to increase specificity @@ -137,7 +151,6 @@ export const Select = styled.select< SelectProps >` width: 100%; max-width: none; cursor: pointer; - overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -145,6 +158,7 @@ export const Select = styled.select< SelectProps >` ${ fontSizeStyles }; ${ sizeStyles }; ${ sizePaddings }; + ${ overflowStyles } } `; diff --git a/packages/components/src/select-control/types.ts b/packages/components/src/select-control/types.ts index 6126ba5043d6c..2b8d187d4031e 100644 --- a/packages/components/src/select-control/types.ts +++ b/packages/components/src/select-control/types.ts @@ -23,7 +23,9 @@ export interface SelectControlProps >, Pick< BaseControlProps, 'help' | '__nextHasNoMarginBottom' > { /** - * If this property is added, multiple values can be selected. The value passed should be an array. + * If this property is added, multiple values can be selected. The `value` passed should be an array. + * + * In most cases, it is preferable to use the `FormTokenField` or `CheckboxControl` components instead. * * @default false */ diff --git a/packages/components/src/tools-panel/stories/index.js b/packages/components/src/tools-panel/stories/index.tsx similarity index 60% rename from packages/components/src/tools-panel/stories/index.js rename to packages/components/src/tools-panel/stories/index.tsx index 0ff9824406a91..52c89a6ece5d6 100644 --- a/packages/components/src/tools-panel/stories/index.js +++ b/packages/components/src/tools-panel/stories/index.tsx @@ -1,50 +1,73 @@ /** * External dependencies */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import styled from '@emotion/styled'; /** * WordPress dependencies */ import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ import { - __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, -} from '@wordpress/components'; + ToggleGroupControl, + ToggleGroupControlOption, +} from '../../toggle-group-control'; /** * Internal dependencies */ -import { ToolsPanel, ToolsPanelItem } from '../'; +import { ToolsPanel, ToolsPanelItem } from '..'; import Panel from '../../panel'; import UnitControl from '../../unit-control'; import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill'; -export default { +const meta: ComponentMeta< typeof ToolsPanel > = { title: 'Components (Experimental)/ToolsPanel', component: ToolsPanel, + subcomponents: { + ToolsPanelItem, + }, + argTypes: { + as: { control: { type: null } }, + children: { control: { type: null } }, + panelId: { control: { type: null } }, + resetAll: { action: 'resetAll' }, + }, + parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { + expanded: true, + }, + docs: { source: { state: 'open' } }, + }, }; - -export const _default = () => { - const [ height, setHeight ] = useState(); - const [ minHeight, setMinHeight ] = useState(); - const [ width, setWidth ] = useState(); - const [ scale, setScale ] = useState(); - - const resetAll = () => { +export default meta; + +export const Default: ComponentStory< typeof ToolsPanel > = ( { + resetAll: resetAllProp, + ...props +} ) => { + const [ height, setHeight ] = useState< string | undefined >(); + const [ minHeight, setMinHeight ] = useState< string | undefined >(); + const [ width, setWidth ] = useState< string | undefined >(); + const [ scale, setScale ] = useState< React.ReactText | undefined >(); + + const resetAll: typeof resetAllProp = ( filters ) => { setHeight( undefined ); setWidth( undefined ); setMinHeight( undefined ); setScale( undefined ); + resetAllProp( filters ); }; return ( - + !! width } label="Width" @@ -112,23 +135,27 @@ export const _default = () => { ); }; +Default.args = { + label: 'Tools Panel (default example)', +}; -export const WithNonToolsPanelItems = () => { - const [ height, setHeight ] = useState(); - const [ width, setWidth ] = useState(); +export const WithNonToolsPanelItems: ComponentStory< typeof ToolsPanel > = ( { + resetAll: resetAllProp, + ...props +} ) => { + const [ height, setHeight ] = useState< string | undefined >(); + const [ width, setWidth ] = useState< string | undefined >(); - const resetAll = () => { + const resetAll: typeof resetAllProp = ( filters ) => { setHeight( undefined ); setWidth( undefined ); + resetAllProp( filters ); }; return ( - + This text illustrates not all items must be wrapped in a ToolsPanelItem and represented in the panel menu. @@ -162,81 +189,127 @@ export const WithNonToolsPanelItems = () => { ); }; +WithNonToolsPanelItems.args = { + ...Default.args, + label: 'ToolsPanel (with non-menu items)', +}; -export const WithOptionalItemsPlusIcon = ( { isShownByDefault } ) => { - const [ height, setHeight ] = useState(); - const [ width, setWidth ] = useState(); - const [ minWidth, setMinWidth ] = useState(); - - const resetAll = () => { +export const WithOptionalItemsPlusIcon: ComponentStory< + typeof ToolsPanel +> = ( { resetAll: resetAllProp, ...props } ) => { + const [ + isFirstToolsPanelItemShownByDefault, + setIsFirstToolsPanelItemShownByDefault, + ] = useState( false ); + const [ height, setHeight ] = useState< string | undefined >(); + const [ width, setWidth ] = useState< string | undefined >(); + const [ minWidth, setMinWidth ] = useState< string | undefined >(); + + const resetAll: typeof resetAllProp = ( filters ) => { setHeight( undefined ); setWidth( undefined ); setMinWidth( undefined ); + resetAllProp( filters ); }; return ( - - - - !! minWidth } - label="Minimum width" - onDeselect={ () => setMinWidth( undefined ) } - isShownByDefault={ isShownByDefault } + <> + + + - !! minWidth } label="Minimum width" - value={ minWidth } - onChange={ ( next ) => setMinWidth( next ) } - /> - - !! width } - label="Width" - onDeselect={ () => setWidth( undefined ) } - isShownByDefault={ false } - > - setMinWidth( undefined ) } + isShownByDefault={ + isFirstToolsPanelItemShownByDefault + } + > + setMinWidth( next ) } + /> + + !! width } label="Width" - value={ width } - onChange={ ( next ) => setWidth( next ) } - /> - - !! height } - label="Height" - onDeselect={ () => setHeight( undefined ) } - isShownByDefault={ false } - > - setWidth( undefined ) } + isShownByDefault={ false } + > + setWidth( next ) } + /> + + !! height } label="Height" - value={ height } - onChange={ ( next ) => setHeight( next ) } - /> - - - - + onDeselect={ () => setHeight( undefined ) } + isShownByDefault={ false } + > + setHeight( next ) } + /> + + + + + + + ); }; WithOptionalItemsPlusIcon.args = { - isShownByDefault: false, + ...Default.args, + label: 'Tools Panel (optional items only)', }; const { Fill: ToolsPanelItems, Slot } = createSlotFill( 'ToolsPanelSlot' ); -const panelId = 'unique-tools-panel-id'; -export const WithSlotFillItems = () => { - const [ attributes, setAttributes ] = useState( {} ); +export const WithSlotFillItems: ComponentStory< typeof ToolsPanel > = ( { + resetAll: resetAllProp, + panelId, + ...props +} ) => { + const [ attributes, setAttributes ] = useState< { + width?: string; + height?: string; + } >( {} ); const { width, height } = attributes; - const resetAll = ( resetFilters = [] ) => { - let newAttributes = {}; + const resetAll: typeof resetAllProp = ( resetFilters = [] ) => { + let newAttributes: typeof attributes = {}; resetFilters.forEach( ( resetFilter ) => { newAttributes = { @@ -246,9 +319,10 @@ export const WithSlotFillItems = () => { } ); setAttributes( newAttributes ); + resetAllProp( resetFilters ); }; - const updateAttribute = ( name, value ) => { + const updateAttribute = ( name: string, value?: any ) => { setAttributes( { ...attributes, [ name ]: value, @@ -304,7 +378,7 @@ export const WithSlotFillItems = () => { @@ -315,13 +389,23 @@ export const WithSlotFillItems = () => { ); }; +WithSlotFillItems.args = { + ...Default.args, + label: 'Tools Panel With SlotFill Items', + panelId: 'unique-tools-panel-id', +}; -export const WithConditionalDefaultControl = () => { - const [ attributes, setAttributes ] = useState( {} ); +export const WithConditionalDefaultControl: ComponentStory< + typeof ToolsPanel +> = ( { resetAll: resetAllProp, panelId, ...props } ) => { + const [ attributes, setAttributes ] = useState< { + height?: string; + scale?: React.ReactText; + } >( {} ); const { height, scale } = attributes; - const resetAll = ( resetFilters = [] ) => { - let newAttributes = {}; + const resetAll: typeof resetAllProp = ( resetFilters = [] ) => { + let newAttributes: typeof attributes = {}; resetFilters.forEach( ( resetFilter ) => { newAttributes = { @@ -331,9 +415,11 @@ export const WithConditionalDefaultControl = () => { } ); setAttributes( newAttributes ); + + resetAllProp( resetFilters ); }; - const updateAttribute = ( name, value ) => { + const updateAttribute = ( name: string, value?: any ) => { setAttributes( { ...attributes, [ name ]: value, @@ -388,7 +474,7 @@ export const WithConditionalDefaultControl = () => { @@ -399,13 +485,23 @@ export const WithConditionalDefaultControl = () => { ); }; +WithConditionalDefaultControl.args = { + ...Default.args, + label: 'Tools Panel With Conditional Default via SlotFill', + panelId: 'unique-tools-panel-id', +}; -export const WithConditionallyRenderedControl = () => { - const [ attributes, setAttributes ] = useState( {} ); +export const WithConditionallyRenderedControl: ComponentStory< + typeof ToolsPanel +> = ( { resetAll: resetAllProp, panelId, ...props } ) => { + const [ attributes, setAttributes ] = useState< { + height?: string; + scale?: React.ReactText; + } >( {} ); const { height, scale } = attributes; - const resetAll = ( resetFilters = [] ) => { - let newAttributes = {}; + const resetAll: typeof resetAllProp = ( resetFilters = [] ) => { + let newAttributes: typeof attributes = {}; resetFilters.forEach( ( resetFilter ) => { newAttributes = { @@ -415,9 +511,11 @@ export const WithConditionallyRenderedControl = () => { } ); setAttributes( newAttributes ); + + resetAllProp( resetFilters ); }; - const updateAttribute = ( name, value ) => { + const updateAttribute = ( name: string, value?: any ) => { setAttributes( { ...attributes, [ name ]: value, @@ -485,7 +583,7 @@ export const WithConditionallyRenderedControl = () => { @@ -496,8 +594,11 @@ export const WithConditionallyRenderedControl = () => { ); }; - -export { ToolsPanelWithItemGroupSlot } from './utils/tools-panel-with-item-group-slot'; +WithConditionallyRenderedControl.args = { + ...Default.args, + label: 'Tools Panel With Conditionally Rendered Item via SlotFill', + panelId: 'unique-tools-panel-id', +}; const PanelWrapperView = styled.div` font-size: 13px; diff --git a/packages/components/src/tools-panel/stories/utils/tools-panel-with-item-group-slot.js b/packages/components/src/tools-panel/stories/utils/tools-panel-with-item-group-slot.js deleted file mode 100644 index d1895324df4c3..0000000000000 --- a/packages/components/src/tools-panel/stories/utils/tools-panel-with-item-group-slot.js +++ /dev/null @@ -1,246 +0,0 @@ -/** - * External dependencies - */ -import styled from '@emotion/styled'; -import { css } from '@emotion/react'; - -/** - * WordPress dependencies - */ -import { useContext, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import Button from '../../../button'; -import ColorIndicator from '../../../color-indicator'; -import ColorPalette from '../../../color-palette'; -import Dropdown from '../../../dropdown'; -import Panel from '../../../panel'; -import { FlexItem } from '../../../flex'; -import { HStack } from '../../../h-stack'; -import { Item, ItemGroup } from '../../../item-group'; -import { ToolsPanel, ToolsPanelItem, ToolsPanelContext } from '../..'; -import { - createSlotFill, - Provider as SlotFillProvider, -} from '../../../slot-fill'; -import { useCx } from '../../../utils'; - -// Available border colors. -const colors = [ - { name: 'Gray 0', color: '#f6f7f7' }, - { name: 'Gray 5', color: '#dcdcde' }, - { name: 'Gray 20', color: '#a7aaad' }, - { name: 'Gray 70', color: '#3c434a' }, - { name: 'Gray 100', color: '#101517' }, - { name: 'Blue 20', color: '#72aee6' }, - { name: 'Blue 40', color: '#3582c4' }, - { name: 'Blue 70', color: '#0a4b78' }, - { name: 'Red 40', color: '#e65054' }, - { name: 'Red 70', color: '#8a2424' }, - { name: 'Green 10', color: '#68de7c' }, - { name: 'Green 40', color: '#00a32a' }, - { name: 'Green 60', color: '#007017' }, - { name: 'Yellow 10', color: '#f2d675' }, - { name: 'Yellow 40', color: '#bd8600' }, -]; -const panelId = 'unique-tools-panel-id'; - -const { Fill, Slot } = createSlotFill( 'ToolsPanelSlot' ); - -// This storybook example aims to replicate a virtual bubbling SlotFill use case -// for the `ToolsPanel` when the Slot itself is an `ItemGroup`. - -// In this scenario the `ToolsPanel` has to render item placeholders so fills -// maintain their order in the DOM. These placeholders in the DOM prevent the -// normal styling of the `ItemGroup` in particular the border radii on the first -// and last items. In case consumers of the ItemGroup and ToolsPanel are -// applying their own styles to these components, the ToolsPanel needs to assist -// consumers in identifying which of its visible items are first and last. - -// This custom fill is required to re-establish the ToolsPanelContext for -// injected ToolsPanelItem components as they will not have access to the React -// Context as the Provider is part of the ToolsPanelItems.Slot tree. -const ToolsPanelItems = ( { children } ) => { - return ( - - { ( fillProps ) => ( - - { children } - - ) } - - ); -}; - -// This fetches the ToolsPanelContext and passes it through `fillProps` so that -// rendered fills can re-establish the `ToolsPanelContext.Provider`. -const SlotContainer = ( { Slot: ToolsPanelSlot, ...props } ) => { - const toolsPanelContext = useContext( ToolsPanelContext ); - - return ( - - ); -}; - -// This wraps the slot with a `ToolsPanel` mimicking a real-world use case from -// the block editor. -ToolsPanelItems.Slot = ( { resetAll, ...props } ) => ( - - - -); - -export const ToolsPanelWithItemGroupSlot = () => { - const [ attributes, setAttributes ] = useState( {} ); - const { text, background, link } = attributes; - - const cx = useCx(); - const slotWrapperClassName = cx( SlotWrapper ); - const itemClassName = cx( ToolsPanelItemClass ); - - const resetAll = ( resetFilters = [] ) => { - let newAttributes = {}; - - resetFilters.forEach( ( resetFilter ) => { - newAttributes = { - ...newAttributes, - ...resetFilter( newAttributes ), - }; - } ); - - setAttributes( newAttributes ); - }; - - const updateAttribute = ( name, value ) => { - setAttributes( { - ...attributes, - [ name ]: value, - } ); - }; - - const ToolsPanelColorDropdown = ( { attribute, label, value } ) => { - return ( - !! value } - label={ label } - onDeselect={ () => updateAttribute( attribute, undefined ) } - resetAllFilter={ () => ( { [ attribute ]: undefined } ) } - panelId={ panelId } - as={ Item } - > - ( - - ) } - renderContent={ () => ( - - updateAttribute( attribute, newColor ) - } - /> - ) } - /> - - ); - }; - - // ToolsPanelItems are rendered via two different fills to simulate - // injection from multiple locations. - return ( - - - - - - - - - - - - - - - ); -}; - -const PanelWrapperView = styled.div` - font-size: 13px; - - .components-dropdown-menu__menu { - max-width: 220px; - } -`; - -const SlotWrapper = css` - &&& { - row-gap: 0; - border-radius: 20px; - } - - > div { - grid-column: span 2; - border-radius: inherit; - } -`; - -const ToolsPanelItemClass = css` - padding: 0; - - &&.first { - border-top-left-radius: inherit; - border-top-right-radius: inherit; - } - - &.last { - border-bottom-left-radius: inherit; - border-bottom-right-radius: inherit; - border-bottom-color: transparent; - } - && > div, - && > div > button { - width: 100%; - border-radius: inherit; - } -`; diff --git a/packages/components/src/tools-panel/tools-panel-item/README.md b/packages/components/src/tools-panel/tools-panel-item/README.md index 85d03f96d1d04..91f9c78ff9cbe 100644 --- a/packages/components/src/tools-panel/tools-panel-item/README.md +++ b/packages/components/src/tools-panel/tools-panel-item/README.md @@ -31,7 +31,8 @@ This prop identifies the current item as being displayed by default. This means it will show regardless of whether it has a value set or is toggled on in the panel's menu. -- Required: Yes +- Required: No +- Default: `false` ### `label`: `string` @@ -58,18 +59,19 @@ A callback to take action when this item is selected in the `ToolsPanel` menu. - Required: No -### `panelId`: `string` +### `panelId`: `string | null` Panel items will ensure they are only registering with their intended panel by -comparing the `panelId` props set on both the item and the panel itself. This +comparing the `panelId` props set on both the item and the panel itself, or if the `panelId` is explicitly `null`. This allows items to be injected from a shared source. - Required: No -### `resetAllFilter`: `() => void` +### `resetAllFilter`: `( attributes?: any ) => any` A `ToolsPanel` will collect each item's `resetAllFilter` and pass an array of these functions through to the panel's `resetAll` callback. They can then be iterated over to perform additional tasks. - Required: No +- Default: `() => {}` diff --git a/packages/components/src/tools-panel/tools-panel-item/component.tsx b/packages/components/src/tools-panel/tools-panel-item/component.tsx index b0cc12c0f19fd..f66d3845ff083 100644 --- a/packages/components/src/tools-panel/tools-panel-item/component.tsx +++ b/packages/components/src/tools-panel/tools-panel-item/component.tsx @@ -13,7 +13,7 @@ import type { ToolsPanelItemProps } from '../types'; // This wraps controls to be conditionally displayed within a tools panel. It // prevents props being applied to HTML elements that would make them invalid. -const ToolsPanelItem = ( +const UnconnectedToolsPanelItem = ( props: WordPressComponentProps< ToolsPanelItemProps, 'div' >, forwardedRef: ForwardedRef< any > ) => { @@ -37,9 +37,9 @@ const ToolsPanelItem = ( ); }; -const ConnectedToolsPanelItem = contextConnect( - ToolsPanelItem, +export const ToolsPanelItem = contextConnect( + UnconnectedToolsPanelItem, 'ToolsPanelItem' ); -export default ConnectedToolsPanelItem; +export default ToolsPanelItem; diff --git a/packages/components/src/tools-panel/tools-panel-item/hook.ts b/packages/components/src/tools-panel/tools-panel-item/hook.ts index c0b5572fee0d1..84572fba451a5 100644 --- a/packages/components/src/tools-panel/tools-panel-item/hook.ts +++ b/packages/components/src/tools-panel/tools-panel-item/hook.ts @@ -13,16 +13,18 @@ import { useContextSystem, WordPressComponentProps } from '../../ui/context'; import { useCx } from '../../utils/hooks/use-cx'; import type { ToolsPanelItemProps } from '../types'; +const noop = () => {}; + export function useToolsPanelItem( props: WordPressComponentProps< ToolsPanelItemProps, 'div' > ) { const { className, hasValue, - isShownByDefault, + isShownByDefault = false, label, panelId, - resetAllFilter, + resetAllFilter = noop, onDeselect, onSelect, ...otherProps diff --git a/packages/components/src/tools-panel/tools-panel/README.md b/packages/components/src/tools-panel/tools-panel/README.md index e04b774e9201b..6802a6436875a 100644 --- a/packages/components/src/tools-panel/tools-panel/README.md +++ b/packages/components/src/tools-panel/tools-panel/README.md @@ -155,6 +155,7 @@ Flags that the items in this ToolsPanel will be contained within an inner wrapper element allowing the panel to lay them out accordingly. - Required: No +- Default: `false` ### `headingLevel`: `1 | 2 | 3 | 4 | 5 | 6 | '1' | '2' | '3' | '4' | '5' | '6'` @@ -170,7 +171,7 @@ panel's dropdown menu. - Required: Yes -### `panelId`: `string` +### `panelId`: `string | null` If a `panelId` is set, it is passed through the `ToolsPanelContext` and used to restrict panel items. When a `panelId` is set, items can only register @@ -179,10 +180,9 @@ exactly. - Required: No -### `resetAll`: `() => void` +### `resetAll`: `( filters?: ResetAllFilter[] ) => void` -A function to call when the `Reset all` menu option is selected. This is passed -through to the panel's header component. +A function to call when the `Reset all` menu option is selected. As an argument, it receives an array containing the `resetAllFilter` callbacks of all the valid registered `ToolsPanelItems`. - Required: Yes @@ -192,3 +192,4 @@ Advises the `ToolsPanel` that all of its `ToolsPanelItem` children should render placeholder content (instead of `null`) when they are toggled off and hidden. - Required: No +- Default: `false` diff --git a/packages/components/src/tools-panel/tools-panel/component.tsx b/packages/components/src/tools-panel/tools-panel/component.tsx index 929868e3f1f8c..c6f3a9ce5469d 100644 --- a/packages/components/src/tools-panel/tools-panel/component.tsx +++ b/packages/components/src/tools-panel/tools-panel/component.tsx @@ -13,7 +13,7 @@ import { Grid } from '../../grid'; import { contextConnect, WordPressComponentProps } from '../../ui/context'; import type { ToolsPanelProps } from '../types'; -const ToolsPanel = ( +const UnconnectedToolsPanel = ( props: WordPressComponentProps< ToolsPanelProps, 'div' >, forwardedRef: ForwardedRef< any > ) => { @@ -42,6 +42,58 @@ const ToolsPanel = ( ); }; -const ConnectedToolsPanel = contextConnect( ToolsPanel, 'ToolsPanel' ); +/** + * The `ToolsPanel` is a container component that displays its children preceded + * by a header. The header includes a dropdown menu which is automatically + * generated from the panel's inner `ToolsPanelItems`. + * + * @example + * ```jsx + * import { __ } from '@wordpress/i18n'; + * import { + * __experimentalToolsPanel as ToolsPanel, + * __experimentalToolsPanelItem as ToolsPanelItem, + * __experimentalUnitControl as UnitControl + * } from '@wordpress/components'; + * + * function Example() { + * const [ height, setHeight ] = useState(); + * const [ width, setWidth ] = useState(); + * + * const resetAll = () => { + * setHeight(); + * setWidth(); + * } + * + * return ( + * + * !! height } + * label={ __( 'Height' ) } + * onDeselect={ () => setHeight() } + * > + * + * + * !! width } + * label={ __( 'Width' ) } + * onDeselect={ () => setWidth() } + * > + * + * + * + * ); + * } + * ``` + */ +export const ToolsPanel = contextConnect( UnconnectedToolsPanel, 'ToolsPanel' ); -export default ConnectedToolsPanel; +export default ToolsPanel; diff --git a/packages/components/src/tools-panel/tools-panel/hook.ts b/packages/components/src/tools-panel/tools-panel/hook.ts index 38751ecb951ad..66b91f0ee671f 100644 --- a/packages/components/src/tools-panel/tools-panel/hook.ts +++ b/packages/components/src/tools-panel/tools-panel/hook.ts @@ -59,8 +59,8 @@ export function useToolsPanel( headingLevel = 2, resetAll, panelId, - hasInnerWrapper, - shouldRenderPlaceholderItems, + hasInnerWrapper = false, + shouldRenderPlaceholderItems = false, __experimentalFirstVisibleItemClass, __experimentalLastVisibleItemClass, ...otherProps diff --git a/packages/components/src/tools-panel/types.ts b/packages/components/src/tools-panel/types.ts index ed846c900b18e..86a8c1579b9b5 100644 --- a/packages/components/src/tools-panel/types.ts +++ b/packages/components/src/tools-panel/types.ts @@ -8,7 +8,7 @@ import type { ReactNode } from 'react'; */ import type { HeadingSize } from '../heading/types'; -type ResetAllFilter = () => void; +type ResetAllFilter = ( attributes?: any ) => any; type ResetAll = ( filters?: ResetAllFilter[] ) => void; export type ToolsPanelProps = { @@ -19,8 +19,10 @@ export type ToolsPanelProps = { /** * Flags that the items in this ToolsPanel will be contained within an inner * wrapper element allowing the panel to lay them out accordingly. + * + * @default false */ - hasInnerWrapper: boolean; + hasInnerWrapper?: boolean; /** * The heading level of the panel's header. * @@ -34,20 +36,24 @@ export type ToolsPanelProps = { label: string; /** * If a `panelId` is set, it is passed through the `ToolsPanelContext` and - * used to restrict panel items. Only items with a matching `panelId` will - * be able to register themselves with this panel. + * used to restrict panel items. When a `panelId` is set, items can only + * register themselves if the `panelId` is explicitly `null` or the item's + * `panelId` matches exactly. */ - panelId: string; + panelId?: string | null; /** - * A function to call when the `Reset all` menu option is selected. This is - * passed through to the panel's header component. + * A function to call when the `Reset all` menu option is selected. As an + * argument, it receives an array containing the `resetAllFilter` callbacks + * of all the valid registered `ToolsPanelItems`. */ resetAll: ResetAll; /** * Advises the `ToolsPanel` that its child `ToolsPanelItem`s should render * placeholder content instead of null when they are toggled off and hidden. + * + * @default false */ - shouldRenderPlaceholderItems: boolean; + shouldRenderPlaceholderItems?: boolean; /** * Experimental prop allowing for a custom CSS class to be applied to the * first visible `ToolsPanelItem` within the `ToolsPanel`. @@ -96,8 +102,10 @@ export type ToolsPanelItem = { * This prop identifies the current item as being displayed by default. This * means it will show regardless of whether it has a value set or is toggled * on in the panel's menu. + * + * @default false */ - isShownByDefault: boolean; + isShownByDefault?: boolean; /** * The supplied label is dual purpose. It is used as: * 1. the human-readable label for the panel's dropdown menu @@ -108,17 +116,20 @@ export type ToolsPanelItem = { */ label: string; /** - * Panel items will ensure they are only registering with their intended - * panel by comparing the `panelId` props set on both the item and the panel - * itself. This allows items to be injected from a shared source. + * Panel items will ensure they are only registering with their intended panel + * by comparing the `panelId` props set on both the item and the panel itself, + * or if the `panelId` is explicitly `null`. This allows items to be injected + * from a shared source. */ - panelId: string; + panelId?: string | null; /** * A `ToolsPanel` will collect each item's `resetAllFilter` and pass an * array of these functions through to the panel's `resetAll` callback. They * can then be iterated over to perform additional tasks. + * + * @default noop */ - resetAllFilter: ResetAllFilter; + resetAllFilter?: ResetAllFilter; }; export type ToolsPanelItemProps = ToolsPanelItem & { @@ -145,7 +156,7 @@ export type ToolsPanelMenuItems = { }; export type ToolsPanelContext = { - panelId?: string; + panelId?: string | null; menuItems: ToolsPanelMenuItems; hasMenuItems: boolean; registerPanelItem: ( item: ToolsPanelItem ) => void; diff --git a/packages/compose/src/hooks/use-constrained-tabbing/index.js b/packages/compose/src/hooks/use-constrained-tabbing/index.js index ba637951563f4..97b8a2a0a5eb5 100644 --- a/packages/compose/src/hooks/use-constrained-tabbing/index.js +++ b/packages/compose/src/hooks/use-constrained-tabbing/index.js @@ -45,14 +45,31 @@ function useConstrainedTabbing() { /** @type {HTMLElement} */ ( target ) ) || null; - // If the element that is about to receive focus is outside the - // area, move focus to a div and insert it at the start or end of - // the area, depending on the direction. Without preventing default - // behaviour, the browser will then move focus to the next element. + // When the target element contains the element that is about to + // receive focus, for example when the target is a tabbable + // container, browsers may disagree on where to move focus next. + // In this case we can't rely on native browsers behavior. We need + // to manage focus instead. + // See https://github.com/WordPress/gutenberg/issues/46041. + if ( + /** @type {HTMLElement} */ ( target ).contains( nextElement ) + ) { + event.preventDefault(); + /** @type {HTMLElement} */ ( nextElement )?.focus(); + return; + } + + // If the element that is about to receive focus is inside the + // area, rely on native browsers behavior and let tabbing follow + // the native tab sequence. if ( node.contains( nextElement ) ) { return; } + // If the element that is about to receive focus is outside the + // area, move focus to a div and insert it at the start or end of + // the area, depending on the direction. Without preventing default + // behaviour, the browser will then move focus to the next element. const domAction = shiftKey ? 'append' : 'prepend'; const { ownerDocument } = node; const trap = ownerDocument.createElement( 'div' ); diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/sidebar-editor-provider.js b/packages/customize-widgets/src/components/sidebar-block-editor/sidebar-editor-provider.js index da9a278a42b86..25b541c3d16db 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/sidebar-editor-provider.js +++ b/packages/customize-widgets/src/components/sidebar-block-editor/sidebar-editor-provider.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { experiments as blockEditorExperiments } from '@wordpress/block-editor'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies @@ -11,7 +11,7 @@ import useBlocksFocusControl from '../focus-control/use-blocks-focus-control'; import { unlock } from '../../private-apis'; -const { ExperimentalBlockEditorProvider } = unlock( blockEditorExperiments ); +const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); export default function SidebarEditorProvider( { sidebar, diff --git a/packages/dom/README.md b/packages/dom/README.md index 3241ab46479ec..5aebde58ee140 100644 --- a/packages/dom/README.md +++ b/packages/dom/README.md @@ -135,7 +135,8 @@ _Returns_ ### getScrollContainer -Given a DOM node, finds the closest scrollable container node. +Given a DOM node, finds the closest scrollable container node or the node +itself, if scrollable. _Parameters_ diff --git a/packages/dom/src/dom/get-scroll-container.js b/packages/dom/src/dom/get-scroll-container.js index 3646572503e2f..6c472e6195496 100644 --- a/packages/dom/src/dom/get-scroll-container.js +++ b/packages/dom/src/dom/get-scroll-container.js @@ -4,7 +4,8 @@ import getComputedStyle from './get-computed-style'; /** - * Given a DOM node, finds the closest scrollable container node. + * Given a DOM node, finds the closest scrollable container node or the node + * itself, if scrollable. * * @param {Element | null} node Node from which to start. * diff --git a/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts b/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts index 59d52e6c151c3..dba597a19c983 100644 --- a/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts +++ b/packages/e2e-test-utils-playwright/src/editor/get-blocks.ts @@ -12,10 +12,19 @@ import type { Editor } from './index'; */ export async function getBlocks( this: Editor ) { return await this.page.evaluate( () => { + const blocks = window.wp.data.select( 'core/block-editor' ).getBlocks(); + // The editor might still contain an unmodified empty block even when it's technically "empty". - if ( window.wp.data.select( 'core/editor' ).isEditedPostEmpty() ) { - return []; + if ( blocks.length === 1 ) { + const blockName = blocks[ 0 ].name; + if ( + blockName === window.wp.blocks.getDefaultBlockName() || + blockName === window.wp.blocks.getFreeformContentHandlerName() + ) { + return []; + } } - return window.wp.data.select( 'core/block-editor' ).getBlocks(); + + return blocks; } ); } diff --git a/packages/e2e-test-utils-playwright/src/editor/open-document-settings-sidebar.ts b/packages/e2e-test-utils-playwright/src/editor/open-document-settings-sidebar.ts index 0cb27c33c6c88..bb8c23b16348d 100644 --- a/packages/e2e-test-utils-playwright/src/editor/open-document-settings-sidebar.ts +++ b/packages/e2e-test-utils-playwright/src/editor/open-document-settings-sidebar.ts @@ -2,29 +2,29 @@ * Internal dependencies */ import type { Editor } from './index'; -import { expect } from '../test'; /** - * Clicks on the button in the header which opens Document Settings sidebar when it is closed. + * Clicks on the button in the header which opens Document Settings sidebar when + * it is closed. * * @param {Editor} this */ export async function openDocumentSettingsSidebar( this: Editor ) { - const editorSettingsButton = this.page.locator( - 'role=region[name="Editor top bar"i] >> role=button[name="Settings"i]' - ); + const toggleButton = this.page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { + name: 'Settings', + disabled: false, + } ); - const isEditorSettingsOpened = - ( await editorSettingsButton.getAttribute( 'aria-expanded' ) ) === - 'true'; + const isClosed = + ( await toggleButton.getAttribute( 'aria-expanded' ) ) === 'false'; - if ( ! isEditorSettingsOpened ) { - await editorSettingsButton.click(); - - await expect( - this.page.locator( - 'role=region[name="Editor settings"i] >> role=button[name^="Close settings"i]' - ) - ).toBeVisible(); + if ( isClosed ) { + await toggleButton.click(); + await this.page + .getByRole( 'region', { name: 'Editor settings' } ) + .getByRole( 'button', { name: 'Close settings' } ) + .waitFor(); } } diff --git a/packages/e2e-test-utils-playwright/src/index.ts b/packages/e2e-test-utils-playwright/src/index.ts index eb87d3b798751..9f283017f1016 100644 --- a/packages/e2e-test-utils-playwright/src/index.ts +++ b/packages/e2e-test-utils-playwright/src/index.ts @@ -3,5 +3,4 @@ export { Admin } from './admin'; export { Editor } from './editor'; export { PageUtils } from './page-utils'; export { RequestUtils } from './request-utils'; -export { SiteEditor } from './site-editor'; export { test, expect } from './test'; diff --git a/packages/e2e-test-utils-playwright/src/request-utils/index.ts b/packages/e2e-test-utils-playwright/src/request-utils/index.ts index 5c6f849f86751..38fb0b9c3faa6 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/index.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/index.ts @@ -21,6 +21,12 @@ import { activateTheme } from './themes'; import { deleteAllBlocks } from './blocks'; import { createComment, deleteAllComments } from './comments'; import { createPost, deleteAllPosts } from './posts'; +import { + createClassicMenu, + createNavigationMenu, + deleteAllMenus, +} from './menus'; +import { deleteAllPages } from './pages'; import { resetPreferences } from './preferences'; import { getSiteSettings, updateSiteSettings } from './site-settings'; import { deleteAllWidgets, addWidgetBlock } from './widgets'; @@ -125,6 +131,9 @@ class RequestUtils { deleteAllBlocks = deleteAllBlocks; createPost = createPost.bind( this ); deleteAllPosts = deleteAllPosts.bind( this ); + createClassicMenu = createClassicMenu.bind( this ); + createNavigationMenu = createNavigationMenu.bind( this ); + deleteAllMenus = deleteAllMenus.bind( this ); createComment = createComment.bind( this ); deleteAllComments = deleteAllComments.bind( this ); deleteAllWidgets = deleteAllWidgets.bind( this ); @@ -139,6 +148,7 @@ class RequestUtils { deleteAllUsers = deleteAllUsers.bind( this ); getSiteSettings = getSiteSettings.bind( this ); updateSiteSettings = updateSiteSettings.bind( this ); + deleteAllPages = deleteAllPages.bind( this ); } export type { StorageState }; diff --git a/packages/e2e-test-utils-playwright/src/request-utils/menus.ts b/packages/e2e-test-utils-playwright/src/request-utils/menus.ts new file mode 100644 index 0000000000000..e5da27e032c79 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request-utils/menus.ts @@ -0,0 +1,108 @@ +/** + * Internal dependencies + */ +import type { RequestUtils } from './index'; + +export interface MenuData { + title: string; + content: string; +} +export interface NavigationMenu { + id: number; + content: string; + status: 'publish' | 'future' | 'draft' | 'pending' | 'private'; +} + +/** + * Create a classic menu + * + * @param {string} name Menu name. + * @return {string} Menu content. + */ +export async function createClassicMenu( this: RequestUtils, name: string ) { + const menuItems = [ + { + title: 'Custom link', + url: 'http://localhost:8889/', + type: 'custom', + menu_order: 1, + }, + ]; + + const menu = await this.rest< NavigationMenu >( { + method: 'POST', + path: `/wp/v2/menus/`, + data: { + name, + }, + } ); + + if ( menuItems?.length ) { + await this.batchRest( + menuItems.map( ( menuItem ) => ( { + method: 'POST', + path: `/wp/v2/menu-items`, + body: { + menus: menu.id, + object_id: undefined, + ...menuItem, + parent: undefined, + }, + } ) ) + ); + } + + return menu; +} + +/** + * Create a navigation menu + * + * @param {Object} menuData navigation menu post data. + * @return {string} Menu content. + */ +export async function createNavigationMenu( + this: RequestUtils, + menuData: MenuData +) { + return this.rest( { + method: 'POST', + path: `/wp/v2/navigation/`, + data: { + status: 'publish', + ...menuData, + }, + } ); +} + +/** + * Delete all navigation and classic menus + * + */ +export async function deleteAllMenus( this: RequestUtils ) { + const navMenus = await this.rest< NavigationMenu[] >( { + path: `/wp/v2/navigation/`, + } ); + + if ( navMenus?.length ) { + await this.batchRest( + navMenus.map( ( menu ) => ( { + method: 'DELETE', + path: `/wp/v2/navigation/${ menu.id }?force=true`, + } ) ) + ); + } + + const classicMenus = await this.rest< NavigationMenu[] >( { + path: `/wp/v2/menus/`, + } ); + + if ( classicMenus?.length ) { + await this.batchRest( + classicMenus.map( ( menu ) => ( { + method: 'DELETE', + path: `/wp/v2/menus/${ menu.id }?force=true`, + } ) ) + ); + } +} diff --git a/packages/e2e-test-utils-playwright/src/request-utils/pages.ts b/packages/e2e-test-utils-playwright/src/request-utils/pages.ts new file mode 100644 index 0000000000000..a9d67e2e57b76 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/request-utils/pages.ts @@ -0,0 +1,51 @@ +/** + * Internal dependencies + */ +import type { RequestUtils } from './index'; + +const PAGE_STATUS = [ + 'publish', + 'future', + 'draft', + 'pending', + 'private', + 'trash', +] as const; + +export type Page = { + id: number; + status: typeof PAGE_STATUS[ number ]; +}; + +/** + * Delete all pages using REST API. + * + * @param {RequestUtils} this + */ +export async function deleteAllPages( this: RequestUtils ) { + // List all pages. + // https://developer.wordpress.org/rest-api/reference/pages/#list-pages + const pages = await this.rest< Page[] >( { + path: '/wp/v2/pages', + params: { + per_page: 100, + + status: PAGE_STATUS.join( ',' ), + }, + } ); + + // Delete all pages one by one. + // https://developer.wordpress.org/rest-api/reference/pages/#delete-a-page + // "/wp/v2/pages" not yet supports batch requests. + await Promise.all( + pages.map( ( page ) => + this.rest( { + method: 'DELETE', + path: `/wp/v2/pages/${ page.id }`, + params: { + force: true, + }, + } ) + ) + ); +} diff --git a/packages/e2e-test-utils-playwright/src/request-utils/posts.ts b/packages/e2e-test-utils-playwright/src/request-utils/posts.ts index d4d08eaf0f5b5..667b49f5931de 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/posts.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/posts.ts @@ -10,6 +10,7 @@ export interface Post { } export interface CreatePostPayload { + title?: string; content: string; status: 'publish' | 'future' | 'draft' | 'pending' | 'private'; } diff --git a/packages/e2e-test-utils-playwright/src/site-editor/index.ts b/packages/e2e-test-utils-playwright/src/site-editor/index.ts deleted file mode 100644 index 740dc56d31496..0000000000000 --- a/packages/e2e-test-utils-playwright/src/site-editor/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * External dependencies - */ -import type { Page } from '@playwright/test'; - -/** - * Internal dependencies - */ -import { enterEditMode } from './toggle-canvas-mode'; - -type AdminConstructorProps = { - page: Page; -}; - -export class SiteEditor { - page: Page; - - constructor( { page }: AdminConstructorProps ) { - this.page = page; - } - - enterEditMode = enterEditMode.bind( this ); -} diff --git a/packages/e2e-test-utils-playwright/src/site-editor/toggle-canvas-mode.js b/packages/e2e-test-utils-playwright/src/site-editor/toggle-canvas-mode.js deleted file mode 100644 index 01340eb085d88..0000000000000 --- a/packages/e2e-test-utils-playwright/src/site-editor/toggle-canvas-mode.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Enters the site editor edit mode. - * - * @this {import('.').SiteEditor} - */ -export async function enterEditMode() { - await this.page.click( '.edit-site-site-hub__edit-button' ); -} diff --git a/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts index 5e62c2d3e172c..a277afa1cd13d 100644 --- a/packages/e2e-test-utils-playwright/src/test.ts +++ b/packages/e2e-test-utils-playwright/src/test.ts @@ -8,7 +8,7 @@ import type { ConsoleMessage } from '@playwright/test'; /** * Internal dependencies */ -import { Admin, Editor, PageUtils, RequestUtils, SiteEditor } from './index'; +import { Admin, Editor, PageUtils, RequestUtils } from './index'; const STORAGE_STATE_PATH = process.env.STORAGE_STATE_PATH || @@ -102,7 +102,6 @@ const test = base.extend< editor: Editor; pageUtils: PageUtils; snapshotConfig: void; - siteEditor: SiteEditor; }, { requestUtils: RequestUtils; @@ -114,9 +113,6 @@ const test = base.extend< editor: async ( { page }, use ) => { await use( new Editor( { page } ) ); }, - siteEditor: async ( { page }, use ) => { - await use( new SiteEditor( { page } ) ); - }, page: async ( { page }, use ) => { page.on( 'console', observeConsoleLogging ); @@ -155,24 +151,6 @@ const test = base.extend< }, { scope: 'worker', auto: true }, ], - // An automatic fixture to configure snapshot settings globally. - snapshotConfig: [ - async ( {}, use, testInfo ) => { - // A work-around to remove the default snapshot suffix. - // See https://github.com/microsoft/playwright/issues/11134 - testInfo.snapshotSuffix = ''; - // Normalize snapshots into the same `__snapshots__` folder to minimize - // the file name length on Windows. - // See https://github.com/WordPress/gutenberg/issues/40291 - testInfo.snapshotDir = path.join( - path.dirname( testInfo.file ), - '__snapshots__' - ); - - await use(); - }, - { auto: true }, - ], } ); export { test, expect }; diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index c94cb5154c178..da06a13389044 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -527,6 +527,10 @@ _Returns_ - `Promise`: Promise resolving with a boolean indicating if the focused block is the default block. +### isListViewOpen + +Undocumented declaration. + ### isOfflineMode Undocumented declaration. diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index a52c74cd2130d..22cb9b9fa8185 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -104,7 +104,7 @@ export { rest as __experimentalRest, batch as __experimentalBatch, } from './rest-api'; -export { openListView, closeListView } from './list-view'; +export { isListViewOpen, openListView, closeListView } from './list-view'; export { disableSiteEditorWelcomeGuide, getCurrentSiteEditorContent, diff --git a/packages/e2e-test-utils/src/list-view.js b/packages/e2e-test-utils/src/list-view.js index 58b857652f589..f02b5d1729dcc 100644 --- a/packages/e2e-test-utils/src/list-view.js +++ b/packages/e2e-test-utils/src/list-view.js @@ -5,7 +5,7 @@ async function toggleListView() { ); } -async function isListViewOpen() { +export async function isListViewOpen() { return await page.evaluate( () => { // selector .edit-post-header-toolbar__list-view-toggle is still required because the performance tests also execute against older versions that still use that selector. return !! document.querySelector( diff --git a/packages/e2e-test-utils/src/open-document-settings-sidebar.js b/packages/e2e-test-utils/src/open-document-settings-sidebar.js index 2ab5f773c3715..8d50c8c74b996 100644 --- a/packages/e2e-test-utils/src/open-document-settings-sidebar.js +++ b/packages/e2e-test-utils/src/open-document-settings-sidebar.js @@ -2,12 +2,17 @@ * Clicks on the button in the header which opens Document Settings sidebar when it is closed. */ export async function openDocumentSettingsSidebar() { - const openButton = await page.$( - '.edit-post-header__settings button[aria-label="Settings"][aria-expanded="false"]' + const toggleButton = await page.waitForSelector( + '.edit-post-header__settings button[aria-label="Settings"][aria-disabled="false"]' ); - if ( openButton ) { - await openButton.click(); + const isClosed = await page.evaluate( + ( element ) => element.getAttribute( 'aria-expanded' ) === 'false', + toggleButton + ); + + if ( isClosed ) { + await toggleButton.click(); await page.waitForSelector( '.edit-post-sidebar' ); } } diff --git a/packages/e2e-test-utils/src/site-editor.js b/packages/e2e-test-utils/src/site-editor.js index 425931ccf9cbc..bccbb51057912 100644 --- a/packages/e2e-test-utils/src/site-editor.js +++ b/packages/e2e-test-utils/src/site-editor.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { visitAdminPage } from '@wordpress/e2e-test-utils'; +import { canvas, visitAdminPage } from '@wordpress/e2e-test-utils'; import { addQueryArgs } from '@wordpress/url'; /** @@ -166,11 +166,13 @@ export async function openPreviousGlobalStylesPanel() { * Enters edit mode. */ export async function enterEditMode() { - const editSiteToggle = await page.$( '.edit-site-site-hub__edit-button' ); + const isViewMode = await page.$( + '.edit-site-visual-editor__editor-canvas[role="button"]' + ); // This check is necessary for the performance tests in old branches // where the site editor toggle was not implemented yet. - if ( ! editSiteToggle ) { + if ( ! isViewMode ) { return; } - await page.click( '.edit-site-site-hub__edit-button' ); + await canvas().click( 'body' ); } diff --git a/packages/e2e-tests/config/setup-performance-test.js b/packages/e2e-tests/config/setup-performance-test.js index 172699035571e..384a88b8a3f2d 100644 --- a/packages/e2e-tests/config/setup-performance-test.js +++ b/packages/e2e-tests/config/setup-performance-test.js @@ -19,9 +19,11 @@ const PUPPETEER_TIMEOUT = process.env.PUPPETEER_TIMEOUT; // The Jest timeout is increased because these tests are a bit slow. jest.setTimeout( PUPPETEER_TIMEOUT || 100000 ); -async function setupBrowser() { - await clearLocalStorage(); +async function setupPage() { await setBrowserViewport( 'large' ); + await page.emulateMediaFeatures( [ + { name: 'prefers-reduced-motion', value: 'reduce' }, + ] ); } // Before every test suite run, delete all content created by the test. This ensures @@ -32,13 +34,18 @@ beforeAll( async () => { await trashAllPosts(); await trashAllPosts( 'wp_block' ); - await setupBrowser(); + await clearLocalStorage(); + await setupPage(); await activatePlugin( 'gutenberg-test-plugin-disables-the-css-animations' ); - await page.emulateMediaFeatures( [ - { name: 'prefers-reduced-motion', value: 'reduce' }, - ] ); } ); afterEach( async () => { - await setupBrowser(); + // Clear localStorage between tests so that the next test starts clean. + await clearLocalStorage(); + // Close the previous page entirely and create a new page, so that the next test + // isn't affected by page unload work. + await page.close(); + page = await browser.newPage(); + // Set up testing config on new page. + await setupPage(); } ); diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap deleted file mode 100644 index 68fd617f7d40c..0000000000000 --- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/heading.test.js.snap +++ /dev/null @@ -1,51 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Heading can be created by prefixing existing content with number signs and a space 1`] = ` -" -

4

-" -`; - -exports[`Heading can be created by prefixing number sign and a space 1`] = ` -" -

3

-" -`; - -exports[`Heading should correctly apply named colors 1`] = ` -" -

Heading

-" -`; - -exports[`Heading should create a paragraph block above when pressing enter at the start 1`] = ` -" -

- - - -

a

-" -`; - -exports[`Heading should create a paragraph block below when pressing enter at the end 1`] = ` -" -

a

- - - -

-" -`; - -exports[`Heading should not work with the list input rule 1`] = ` -" -

1. H

-" -`; - -exports[`Heading should work with the format input rules 1`] = ` -" -

code

-" -`; diff --git a/packages/e2e-tests/specs/editor/blocks/heading.test.js b/packages/e2e-tests/specs/editor/blocks/heading.test.js deleted file mode 100644 index fb89bed12accc..0000000000000 --- a/packages/e2e-tests/specs/editor/blocks/heading.test.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * WordPress dependencies - */ -import { - clickBlockAppender, - createNewPost, - getEditedPostContent, - pressKeyWithModifier, -} from '@wordpress/e2e-test-utils'; - -describe( 'Heading', () => { - const COLOR_ITEM_SELECTOR = - '.block-editor-panel-color-gradient-settings__dropdown'; - const CUSTOM_COLOR_BUTTON_X_SELECTOR = `.components-color-palette__custom-color`; - const COLOR_INPUT_FIELD_SELECTOR = - '.components-color-picker .components-input-control__input'; - - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'can be created by prefixing number sign and a space', async () => { - await clickBlockAppender(); - await page.keyboard.type( '### 3' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'can be created by prefixing existing content with number signs and a space', async () => { - await clickBlockAppender(); - await page.keyboard.type( '4' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.type( '#### ' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not work with the list input rule', async () => { - await clickBlockAppender(); - await page.keyboard.type( '## 1. H' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should work with the format input rules', async () => { - await clickBlockAppender(); - await page.keyboard.type( '## `code`' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should create a paragraph block above when pressing enter at the start', async () => { - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '## a' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'Enter' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should create a paragraph block below when pressing enter at the end', async () => { - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '## a' ); - await page.keyboard.press( 'Enter' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should correctly apply custom colors', async () => { - await clickBlockAppender(); - await page.keyboard.type( '### Heading' ); - - const textColorButton = await page.waitForSelector( - COLOR_ITEM_SELECTOR - ); - await textColorButton.click(); - - const customTextColorButton = await page.waitForSelector( - CUSTOM_COLOR_BUTTON_X_SELECTOR - ); - - await customTextColorButton.click(); - await page.waitForSelector( COLOR_INPUT_FIELD_SELECTOR ); - await page.click( COLOR_INPUT_FIELD_SELECTOR ); - await pressKeyWithModifier( 'primary', 'A' ); - await page.keyboard.type( '4b7f4d' ); - await page.keyboard.press( 'Enter' ); - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

Heading

- " - ` ); - } ); - - it( 'should correctly apply named colors', async () => { - await clickBlockAppender(); - await page.keyboard.type( '## Heading' ); - - const textColorButton = await page.waitForSelector( - COLOR_ITEM_SELECTOR - ); - await textColorButton.click(); - - const colorButtonSelector = `//button[@aria-label='Color: Luminous vivid orange']`; - const [ colorButton ] = await page.$x( colorButtonSelector ); - await colorButton.click(); - await page.waitForXPath( - `${ colorButtonSelector }[@aria-pressed='true']` - ); - await page.click( 'h2[data-type="core/heading"]' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/blocks/query.test.js b/packages/e2e-tests/specs/editor/blocks/query.test.js deleted file mode 100644 index d99918a253462..0000000000000 --- a/packages/e2e-tests/specs/editor/blocks/query.test.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - deactivatePlugin, - createNewPost, - insertBlock, - publishPost, - trashAllPosts, -} from '@wordpress/e2e-test-utils'; - -const createDemoPosts = async () => { - await createNewPost( { postType: 'post', title: `Post 1` } ); - await publishPost(); -}; - -describe( 'Query block', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-query-block' ); - await createDemoPosts(); - } ); - afterAll( async () => { - await trashAllPosts(); - await deactivatePlugin( 'gutenberg-test-query-block' ); - } ); - beforeEach( async () => { - await createNewPost( { postType: 'page', title: `Query Page` } ); - } ); - afterEach( async () => { - await trashAllPosts( 'page' ); - } ); - describe( 'Query block insertion', () => { - it( 'List', async () => { - await insertBlock( 'Query' ); - // Wait for the choose pattern button - const choosePatternButton = await page.waitForSelector( - 'div[data-type="core/query"] button.is-primary' - ); - await choosePatternButton.click(); - // Wait for pattern blocks to be loaded. - await page.waitForSelector( - '.block-library-query-pattern__selection-content iframe[title="Editor canvas"]' - ); - // Choose the standard pattern. - const chosenPattern = await page.waitForSelector( - '.block-editor-block-patterns-list__item[aria-label="Standard"]' - ); - chosenPattern.click(); - // Wait for pattern setup to go away. - await page.waitForSelector( - '.block-library-query-pattern__selection-content', - { - hidden: true, - } - ); - /** - * We can't use `getEditedPostContent` easily for now because - * `query` makes used of `instanceId` so it's not very reliable. - * This should be revisited. - */ - await page.waitForSelector( '.wp-block-post-date' ); - await page.waitForSelector( '.wp-block-post-title' ); - } ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/list-view.test.js b/packages/e2e-tests/specs/editor/various/list-view.test.js index dfb3caf1d8aa4..ade8bcf2359a5 100644 --- a/packages/e2e-tests/specs/editor/various/list-view.test.js +++ b/packages/e2e-tests/specs/editor/various/list-view.test.js @@ -5,6 +5,7 @@ import { createNewPost, insertBlock, getEditedPostContent, + isListViewOpen, openListView, pressKeyWithModifier, pressKeyTimes, @@ -327,4 +328,87 @@ describe( 'List view', () => { ); await expect( listViewGroupBlockRight ).toHaveFocus(); } ); + + async function getActiveElementLabel() { + return page.evaluate( + () => + document.activeElement.getAttribute( 'aria-label' ) || + document.activeElement.textContent + ); + } + + // If list view sidebar is open and focus is not inside the sidebar, move focus to the sidebar when using the shortcut. If focus is inside the sidebar, shortcut should close the sidebar. + it( 'ensures the list view global shortcut works properly', async () => { + // Insert some blocks of different types. + await insertBlock( 'Image' ); + await insertBlock( 'Paragraph' ); + await page.keyboard.type( 'Paragraph text.' ); + + // Open list view sidebar. + await pressKeyWithModifier( 'access', 'o' ); + + // Navigate to the image block. + await page.keyboard.press( 'ArrowUp' ); + // Check if image block link in the list view has focus by XPath selector. + const listViewImageBlock = await page.waitForXPath( + '//a[contains(., "Image")]' + ); + await expect( listViewImageBlock ).toHaveFocus(); + // Select the image block in the list view to move focus to it in the canvas. + await page.keyboard.press( 'Enter' ); + + // Check if image block upload button has focus by XPath selector. + const imageBlockUploadButton = await page.waitForXPath( + '//button[contains(text(), "Upload")]' + ); + await expect( imageBlockUploadButton ).toHaveFocus(); + + // Since focus is now at the image block upload button in the canvas, pressing the list view shortcut should bring focus back to the image block in the list view. + await pressKeyWithModifier( 'access', 'o' ); + await expect( listViewImageBlock ).toHaveFocus(); + + // Since focus is now inside the list view, the shortcut should close the sidebar. + await pressKeyWithModifier( 'access', 'o' ); + // Focus should now be on the paragraph block since that is where we opened the list view sidebar. This is not a perfect solution, but current functionality prevents a better way at the moment. Get the current block aria-label and compare. + await expect( await getActiveElementLabel() ).toEqual( + 'Paragraph block' + ); + // List view sidebar should be closed. + await expect( await isListViewOpen() ).toBeFalsy(); + + // Open list view sidebar. + await pressKeyWithModifier( 'access', 'o' ); + + // Focus the list view close button and make sure the shortcut will close the list view. This is to catch a bug where elements could be out of range of the sidebar region. Must shift+tab 3 times to reach cclose button before tabs. + await pressKeyWithModifier( 'shift', 'Tab' ); + await pressKeyWithModifier( 'shift', 'Tab' ); + await pressKeyWithModifier( 'shift', 'Tab' ); + await expect( await getActiveElementLabel() ).toEqual( + 'Close Document Overview Sidebar' + ); + + // Close the list view sidebar. + await pressKeyWithModifier( 'access', 'o' ); + // List view sidebar should be closed. + await expect( await isListViewOpen() ).toBeFalsy(); + + // Open list view sidebar. + await pressKeyWithModifier( 'access', 'o' ); + + // Focus the outline tab and select it. This test ensures the outline tab receives similar focus events based on the shortcut. + await pressKeyWithModifier( 'shift', 'Tab' ); + await expect( await getActiveElementLabel() ).toEqual( 'Outline' ); + await page.keyboard.press( 'Enter' ); + + // From here, tab in to the editor so focus can be checked on return to the outline tab in the sidebar. + await pressKeyTimes( 'Tab', 2 ); + // Focus should be placed on the outline tab button since there is nothing to focus inside the tab itself. + await pressKeyWithModifier( 'access', 'o' ); + await expect( await getActiveElementLabel() ).toEqual( 'Outline' ); + + // Close the list view sidebar. + await pressKeyWithModifier( 'access', 'o' ); + // List view sidebar should be closed. + await expect( await isListViewOpen() ).toBeFalsy(); + } ); } ); diff --git a/packages/e2e-tests/specs/performance/post-editor.test.js b/packages/e2e-tests/specs/performance/post-editor.test.js index 10699f921be51..597f4f36cd9a3 100644 --- a/packages/e2e-tests/specs/performance/post-editor.test.js +++ b/packages/e2e-tests/specs/performance/post-editor.test.js @@ -105,28 +105,43 @@ describe( 'Post Editor Performance', () => { readFile( join( __dirname, '../../assets/large-post.html' ) ) ); await saveDraft(); - let i = 5; + const draftURL = await page.url(); + + // Number of sample measurements to take. + const samples = 5; + // Number of throwaway measurements to perform before recording samples. + // Having at least one helps ensure that caching quirks don't manifest in + // the results. + const throwaway = 1; + + let i = throwaway + samples; while ( i-- ) { - await page.reload(); + await page.close(); + page = await browser.newPage(); + + await page.goto( draftURL ); await page.waitForSelector( '.edit-post-layout', { timeout: 120000, } ); await canvas().waitForSelector( '.wp-block', { timeout: 120000 } ); - const { - serverResponse, - firstPaint, - domContentLoaded, - loaded, - firstContentfulPaint, - firstBlock, - } = await getLoadingDurations(); - results.serverResponse.push( serverResponse ); - results.firstPaint.push( firstPaint ); - results.domContentLoaded.push( domContentLoaded ); - results.loaded.push( loaded ); - results.firstContentfulPaint.push( firstContentfulPaint ); - results.firstBlock.push( firstBlock ); + if ( i < samples ) { + const { + serverResponse, + firstPaint, + domContentLoaded, + loaded, + firstContentfulPaint, + firstBlock, + } = await getLoadingDurations(); + + results.serverResponse.push( serverResponse ); + results.firstPaint.push( firstPaint ); + results.domContentLoaded.push( domContentLoaded ); + results.loaded.push( loaded ); + results.firstContentfulPaint.push( firstContentfulPaint ); + results.firstBlock.push( firstBlock ); + } } } ); diff --git a/packages/e2e-tests/specs/performance/site-editor.test.js b/packages/e2e-tests/specs/performance/site-editor.test.js index e3a2ef86c6932..2e4002bf0bb1c 100644 --- a/packages/e2e-tests/specs/performance/site-editor.test.js +++ b/packages/e2e-tests/specs/performance/site-editor.test.js @@ -30,34 +30,29 @@ import { jest.setTimeout( 1000000 ); +const results = { + serverResponse: [], + firstPaint: [], + domContentLoaded: [], + loaded: [], + firstContentfulPaint: [], + firstBlock: [], + type: [], + typeContainer: [], + focus: [], + inserterOpen: [], + inserterHover: [], + inserterSearch: [], + listViewOpen: [], +}; + +let id; + describe( 'Site Editor Performance', () => { beforeAll( async () => { await activateTheme( 'emptytheme' ); await deleteAllTemplates( 'wp_template' ); await deleteAllTemplates( 'wp_template_part' ); - } ); - afterAll( async () => { - await deleteAllTemplates( 'wp_template' ); - await deleteAllTemplates( 'wp_template_part' ); - await activateTheme( 'twentytwentyone' ); - } ); - - it( 'Loading', async () => { - const results = { - serverResponse: [], - firstPaint: [], - domContentLoaded: [], - loaded: [], - firstContentfulPaint: [], - firstBlock: [], - type: [], - typeContainer: [], - focus: [], - inserterOpen: [], - inserterHover: [], - inserterSearch: [], - listViewOpen: [], - }; const html = readFile( join( __dirname, '../../assets/large-post.html' ) @@ -80,37 +75,73 @@ describe( 'Site Editor Performance', () => { }, html ); await saveDraft(); - const id = await page.evaluate( () => + id = await page.evaluate( () => new URL( document.location ).searchParams.get( 'post' ) ); + } ); + + afterAll( async () => { + await deleteAllTemplates( 'wp_template' ); + await deleteAllTemplates( 'wp_template_part' ); + await activateTheme( 'twentytwentyone' ); + } ); - await visitSiteEditor( { postId: id, postType: 'page' } ); + beforeEach( async () => { + await visitSiteEditor( { + postId: id, + postType: 'page', + path: '/navigation/single', + } ); + } ); + + it( 'Loading', async () => { + const editorURL = await page.url(); - let i = 3; + // Number of sample measurements to take. + const samples = 3; + // Number of throwaway measurements to perform before recording samples. + // Having at least one helps ensure that caching quirks don't manifest in + // the results. + const throwaway = 1; + + let i = throwaway + samples; // Measuring loading time. while ( i-- ) { - await page.reload(); + await page.close(); + page = await browser.newPage(); + + await page.goto( editorURL ); await page.waitForSelector( '.edit-site-visual-editor', { timeout: 120000, } ); await canvas().waitForSelector( '.wp-block', { timeout: 120000 } ); - const { - serverResponse, - firstPaint, - domContentLoaded, - loaded, - firstContentfulPaint, - firstBlock, - } = await getLoadingDurations(); - - results.serverResponse.push( serverResponse ); - results.firstPaint.push( firstPaint ); - results.domContentLoaded.push( domContentLoaded ); - results.loaded.push( loaded ); - results.firstContentfulPaint.push( firstContentfulPaint ); - results.firstBlock.push( firstBlock ); + + if ( i < samples ) { + const { + serverResponse, + firstPaint, + domContentLoaded, + loaded, + firstContentfulPaint, + firstBlock, + } = await getLoadingDurations(); + + results.serverResponse.push( serverResponse ); + results.firstPaint.push( firstPaint ); + results.domContentLoaded.push( domContentLoaded ); + results.loaded.push( loaded ); + results.firstContentfulPaint.push( firstContentfulPaint ); + results.firstBlock.push( firstBlock ); + } } + } ); + + it( 'Typing', async () => { + await page.waitForSelector( '.edit-site-visual-editor', { + timeout: 120000, + } ); + await canvas().waitForSelector( '.wp-block', { timeout: 120000 } ); // Measuring typing performance inside the post content. await canvas().waitForSelector( @@ -121,7 +152,7 @@ describe( 'Site Editor Performance', () => { '[data-type="core/post-content"] [data-type="core/paragraph"]' ); await insertBlock( 'Paragraph' ); - i = 200; + let i = 200; const traceFile = __dirname + '/trace.json'; await page.tracing.start( { path: traceFile, diff --git a/packages/e2e-tests/specs/site-editor/multi-entity-saving.test.js b/packages/e2e-tests/specs/site-editor/multi-entity-saving.test.js index fa039fb10fd2e..30b634712119a 100644 --- a/packages/e2e-tests/specs/site-editor/multi-entity-saving.test.js +++ b/packages/e2e-tests/specs/site-editor/multi-entity-saving.test.js @@ -265,6 +265,7 @@ describe( 'Multi-entity save flow', () => { await visitSiteEditor( { postId: 'emptytheme//index', postType: 'wp_template', + path: '/templates/single', } ); await enterEditMode(); @@ -304,6 +305,7 @@ describe( 'Multi-entity save flow', () => { await visitSiteEditor( { postId: 'emptytheme//index', postType: 'wp_template', + path: '/templates/single', } ); await enterEditMode(); diff --git a/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js b/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js index 3428ddd654023..c1d4a755a7c57 100644 --- a/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js +++ b/packages/e2e-tests/specs/site-editor/settings-sidebar.test.js @@ -69,6 +69,7 @@ describe( 'Settings sidebar', () => { await visitSiteEditor( { postId: 'emptytheme//singular', postType: 'wp_template', + path: '/templates/single', } ); await enterEditMode(); const templateCardAfterNavigation = await getTemplateCard(); diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index aaec62928573e..b5045c96bfcf0 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -37,6 +37,7 @@ "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", "@wordpress/deprecated": "file:../deprecated", + "@wordpress/dom": "^3.20.0", "@wordpress/editor": "file:../editor", "@wordpress/element": "file:../element", "@wordpress/hooks": "file:../hooks", diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index b4d30adc20244..55ffd265a96cf 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -91,10 +91,13 @@ function HeaderToolbar() { /> ); - const openInserter = useCallback( () => { + const toggleInserter = useCallback( () => { if ( isInserterOpened ) { - // Focusing the inserter button closes the inserter popover. + // Focusing the inserter button should close the inserter popover. + // However, there are some cases it won't close when the focus is lost. + // See https://github.com/WordPress/gutenberg/issues/43090 for more details. inserterButton.current.focus(); + setIsInserterOpened( false ); } else { setIsInserterOpened( true ); } @@ -120,7 +123,7 @@ function HeaderToolbar() { variant="primary" isPressed={ isInserterOpened } onMouseDown={ preventDefault } - onClick={ openInserter } + onClick={ toggleInserter } disabled={ ! isInserterEnabled } icon={ plus } label={ showIconLabels ? shortLabel : longLabel } diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap index 0c07df56490d6..2ad8e457bae30 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap @@ -43,856 +43,858 @@ exports[`KeyboardShortcutHelpModal should match snapshot when the modal is activ -
-
    -
  • -
-
-
-

+
- Global shortcuts -

-
    +
  • +
+
+
-
  • -
    - Navigate to the nearest toolbar. -
    -
    - - - Alt - - + - - F10 - - -
    -
  • -
  • +
      -
      - Save your changes. -
      -
      - - - Ctrl - - + - - S - - -
      - -
    • -
      - Undo your last changes. -
      -
      - +
      - Ctrl - - + - - Z - - -
      -
    • -
    • -
      - Redo your last undo. -
      -
      - + + Alt + + + + + F10 + + +
      +
    • +
    • +
      + Save your changes. +
      +
      - Ctrl - - + - - Shift - - + - - Z - - - + + Ctrl + + + + + S + + +
      +
    • +
    • +
      + Undo your last changes. +
      +
      - Ctrl - - + + aria-label="Control + Z" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Ctrl + + + + + Z + + +
      +
    • +
    • +
      + Redo your last undo. +
      +
      - Y - - -
      -
    • -
    -
  • -
    -

    - Selection shortcuts -

    -
      + + Ctrl + + + + + Shift + + + + + Z + + + + + Ctrl + + + + + Y + + + + +
    +
    +
    -
  • -
    - Select all text when typing. Press again to select all blocks. -
    -
    +
      +
    • - + Select all text when typing. Press again to select all blocks. +
    +
    - Ctrl - - + - - A - - -
    -
  • -
  • -
    - Clear selection. -
    -
    - + + Ctrl + + + + + A + + +
    +
  • +
  • +
    + Clear selection. +
    +
    - escape - - -
    -
  • - -
    -
    -

    + + escape + + + + + +

    +
    - Block shortcuts - -
      -
    • -
      - Duplicate the selected block(s). -
      -
      - - - Ctrl - - + - - Shift - - + - - D - - -
      -
    • -
    • +
        -
        - Remove the selected block(s). -
        -
        - - - Shift - - + - - Alt - - + - - Z - - -
        - -
      • -
        - Insert a new block before the selected block(s). -
        -
        - +
        - Ctrl - - + - - Alt - - + - - T - - -
        -
      • -
      • -
        - Insert a new block after the selected block(s). -
        -
        - + + Ctrl + + + + + Shift + + + + + D + + +
        +
      • +
      • +
        - - Ctrl - - + - - Alt - - + - - Y - - -
        -
      • -
      • -
        - Delete selection. -
        -
        - +
        - del - - - + + Shift + + + + + Alt + + + + + Z + + +
        +
      • +
      • +
        - - backspace - - -
        -
      • -
      • -
        - Move the selected block(s) up. -
        -
        - +
        - Ctrl - - + + aria-label="Control + Alt + T" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Ctrl + + + + + Alt + + + + + T + + +
        +
      • +
      • +
        + Insert a new block after the selected block(s). +
        +
        - Shift - - + + aria-label="Control + Alt + Y" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Ctrl + + + + + Alt + + + + + Y + + +
        +
      • +
      • +
        + Delete selection. +
        +
        - Alt + + del + - + - T + + backspace + - -
        -
      • -
      • -
        - Move the selected block(s) down. -
        -
        +
      • +
      • - + Move the selected block(s) up. + +
        - Ctrl - - + - - Shift - - + - - Alt - - + + aria-label="Control + Shift + Alt + T" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Ctrl + + + + + Shift + + + + + Alt + + + + + T + + +
        +
      • +
      • +
        + Move the selected block(s) down. +
        +
        - Y - - -
        -
      • -
      • -
        - Change the block type after adding a new paragraph. -
        -
        - + + Ctrl + + + + + Shift + + + + + Alt + + + + + Y + + +
        +
      • +
      • +
        + Change the block type after adding a new paragraph. +
        +
        - / - - -
        -
      • -
      -
    -
    -

    + + / + + + + + +

    +
    - Text formatting - -
      -
    • -
      - Make the selected text bold. -
      -
      - - - Ctrl - - + - - B - - -
      -
    • -
    • +
        -
        - Make the selected text italic. -
        -
        - - - Ctrl - - + - - I - - -
        - -
      • -
        - Convert the selected text into a link. -
        -
        - +
        - Ctrl - - + - - K - - -
        -
      • -
      • -
        - Remove a link. -
        -
        - + + Ctrl + + + + + B + + +
        +
      • +
      • +
        - - Ctrl - - + - - Shift - - + - - K - - -
        -
      • -
      • -
        - Insert a link to a post or page. -
        -
        - +
        - [[ - - -
        -
      • -
      • -
        - Underline the selected text. -
        -
        - + + Ctrl + + + + + I + + +
        +
      • +
      • +
        - - Ctrl - - + - - U - - -
        -
      • -
      • -
        - Strikethrough the selected text. -
        -
        - +
        - Shift - - + - - Alt - - + - - D - - -
        -
      • -
      • -
        - Make the selected text inline code. -
        -
        - + + Ctrl + + + + + K + + +
        +
      • +
      • +
        + Remove a link. +
        +
        - Shift - - + - - Alt - - + + aria-label="Control + Shift + K" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Ctrl + + + + + Shift + + + + + K + + +
        +
      • +
      • +
        + Insert a link to a post or page. +
        +
        - X + + [[ + - -
        -
      • -
      • -
        - Convert the current heading to a paragraph. -
        -
        +
      • +
      • - + Underline the selected text. + +
        - Shift - - + - - Alt - - + + aria-label="Control + U" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Ctrl + + + + + U + + +
        +
      • +
      • +
        + Strikethrough the selected text. +
        +
        - 0 - - -
        -
      • -
      • -
        - Convert the current paragraph or heading to a heading of level 1 to 6. -
        -
        - + + Shift + + + + + Alt + + + + + D + + +
        +
      • +
      • +
        + Make the selected text inline code. +
        +
        - Shift - - + + aria-label="Shift + Alt + X" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Shift + + + + + Alt + + + + + X + + +
        +
      • +
      • +
        + Convert the current heading to a paragraph. +
        +
        - Alt - - + + aria-label="Shift + Alt + 0" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Shift + + + + + Alt + + + + + 0 + + +
        +
      • +
      • +
        + Convert the current paragraph or heading to a heading of level 1 to 6. +
        +
        - 1-6 - - -
        -
      • -
      -
    + aria-label="Shift + Alt + 1 6" + class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" + > + + Shift + + + + + Alt + + + + + 1-6 + + + + + + + `; diff --git a/packages/edit-post/src/components/keyboard-shortcuts/index.js b/packages/edit-post/src/components/keyboard-shortcuts/index.js index d3d6569e398a4..f3a8c15addb5b 100644 --- a/packages/edit-post/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-post/src/components/keyboard-shortcuts/index.js @@ -250,9 +250,12 @@ function KeyboardShortcuts() { } } ); - useShortcut( 'core/edit-post/toggle-list-view', () => - setIsListViewOpened( ! isListViewOpened() ) - ); + // Only opens the list view. Other functionality for this shortcut happens in the rendered sidebar. + useShortcut( 'core/edit-post/toggle-list-view', () => { + if ( ! isListViewOpened() ) { + setIsListViewOpened( true ); + } + } ); useShortcut( 'core/block-editor/transform-heading-to-paragraph', diff --git a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap index c0d2d8b7a6b73..67d5ee4e18436 100644 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap @@ -102,425 +102,427 @@ exports[`EditPostPreferencesModal should match snapshot when the modal is active -
    +
    - - - -
    -
    -
    +
    - -

    - Publishing -

    -

    - Change options related to publishing. -

    -
    -
    +

    + Publishing +

    +

    + Change options related to publishing. +

    +
    - - - - - - + + + + + + +
    +

    + Review settings, such as visibility and tags. +

    -

    - Review settings, such as visibility and tags. -

    -
    - -
    - +
    -

    - Appearance -

    -

    - Customize options related to the block editor interface and editing flow. -

    - -
    +

    + Appearance +

    +

    + Customize options related to the block editor interface and editing flow. +

    +
    - - - - - - + + + + + + +
    +

    + Reduce visual distractions by hiding the toolbar and other elements to focus on writing. +

    -

    - Reduce visual distractions by hiding the toolbar and other elements to focus on writing. -

    -
    -
    - - - - - - + + + + + + +
    +

    + Highlights the current block and fades other content. +

    -

    - Highlights the current block and fades other content. -

    -
    -
    - - - - - - + + + + + + +
    +

    + Show text instead of icons on buttons. +

    -

    - Show text instead of icons on buttons. -

    - -
    - - - - - - + + + + + + +
    +

    + Opens the block list view sidebar by default. +

    -

    - Opens the block list view sidebar by default. -

    - -
    - - - - - - + + + + + + +
    +

    + Make the editor look like your theme. +

    -

    - Make the editor look like your theme. -

    - -
    - - - - - - + + + + + + +
    +

    + Shows block breadcrumbs at the bottom of the editor. +

    -

    - Shows block breadcrumbs at the bottom of the editor. -

    - - + + @@ -721,198 +723,200 @@ exports[`EditPostPreferencesModal should match snapshot when the modal is active -
    +
    -
    - -
    -
    -
    +
    -
    - - Blocks - -
    -
    -
    +
    - - + +
    -
    - -
    -
    -
    +
    -
    - - Panels - -
    -
    -
    +
    - - + +
    -
    - + +
    + diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js index 447fcbd1a70db..21c3885f590a7 100644 --- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js @@ -14,10 +14,12 @@ import { useMergeRefs, } from '@wordpress/compose'; import { useDispatch } from '@wordpress/data'; +import { focus } from '@wordpress/dom'; +import { useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { closeSmall } from '@wordpress/icons'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { ESCAPE } from '@wordpress/keycodes'; -import { useState } from '@wordpress/element'; /** * Internal dependencies @@ -31,6 +33,7 @@ export default function ListViewSidebar() { const focusOnMountRef = useFocusOnMount( 'firstElement' ); const headerFocusReturnRef = useFocusReturn(); const contentFocusReturnRef = useFocusReturn(); + function closeOnEscape( event ) { if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) { event.preventDefault(); @@ -40,12 +43,63 @@ export default function ListViewSidebar() { const [ tab, setTab ] = useState( 'list-view' ); + // This ref refers to the sidebar as a whole. + const sidebarRef = useRef(); + // This ref refers to the list view tab button. + const listViewTabRef = useRef(); + // This ref refers to the outline tab button. + const outlineTabRef = useRef(); + // This ref refers to the list view application area. + const listViewRef = useRef(); + + /* + * Callback function to handle list view or outline focus. + * + * @param {string} currentTab The current tab. Either list view or outline. + * + * @return void + */ + function handleSidebarFocus( currentTab ) { + // List view tab is selected. + if ( currentTab === 'list-view' ) { + // Either focus the list view or the list view tab button. Must have a fallback because the list view does not render when there are no blocks. + const listViewApplicationFocus = focus.tabbable.find( + listViewRef.current + )[ 0 ]; + const listViewFocusArea = sidebarRef.current.contains( + listViewApplicationFocus + ) + ? listViewApplicationFocus + : listViewTabRef.current; + listViewFocusArea.focus(); + // Outline tab is selected. + } else { + outlineTabRef.current.focus(); + } + } + + // This only fires when the sidebar is open because of the conditional rendering. It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed. + useShortcut( 'core/edit-post/toggle-list-view', () => { + // If the sidebar has focus, it is safe to close. + if ( + sidebarRef.current.contains( + sidebarRef.current.ownerDocument.activeElement + ) + ) { + setIsListViewOpened( false ); + // If the list view or outline does not have focus, focus should be moved to it. + } else { + handleSidebarFocus( tab ); + } + } ); + return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
  • + } + content={ + post ? decodeEntities( post?.description?.rendered ) : null + } + /> + ); +} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js index c25c0e1956f02..9e939d2392b18 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js @@ -25,6 +25,7 @@ export default function SidebarNavigationScreenNavigationMenus() { history.push( { postType: attributes.type, postId: attributes.id, + path: '/navigation/single', } ); } }, @@ -33,7 +34,6 @@ export default function SidebarNavigationScreenNavigationMenus() { return ( diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/style.scss index 81196bac163ae..86a0c19534fbd 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/style.scss @@ -1,8 +1,4 @@ .edit-site-sidebar-navigation-screen-navigation-menus { - .block-editor-list-view-block__menu-edit, - .edit-site-navigation-inspector__select-menu { - display: none; - } .offcanvas-editor-list-view-leaf { max-width: calc(100% - #{ $grid-unit-05 }); } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js new file mode 100644 index 0000000000000..043cee5dd3f2a --- /dev/null +++ b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; +import { Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import SidebarNavigationScreen from '../sidebar-navigation-screen'; +import useEditedEntityRecord from '../use-edited-entity-record'; +import { unlock } from '../../private-apis'; +import { store as editSiteStore } from '../../store'; + +const config = { + wp_template: { + path: '/templates/single', + }, + wp_template_part: { + path: '/template-parts/single', + }, +}; + +export default function SidebarNavigationScreenTemplate( { + postType = 'wp_template', +} ) { + const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); + const { getDescription, getTitle, record } = useEditedEntityRecord(); + let description = getDescription(); + if ( ! description && record.is_custom ) { + description = __( + 'This is a custom template that can be applied manually to any Post or Page.' + ); + } + + return ( + setCanvasMode( 'edit' ) } + > + { __( 'Edit' ) } + + } + content={ description ?

    { description }

    : undefined } + /> + ); +} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/index.js new file mode 100644 index 0000000000000..b843a9f7d3b6a --- /dev/null +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/index.js @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import SidebarNavigationScreen from '../sidebar-navigation-screen'; + +const config = { + wp_template: { + path: '/templates/all', + title: __( 'All templates' ), + }, + wp_template_part: { + path: '/template-parts/all', + title: __( 'All template parts' ), + }, +}; + +export default function SidebarNavigationScreenTemplatesBrowse( { + postType = 'wp_template', +} ) { + return ( + + ); +} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js index 138cb8ed7bc38..4c1fd155ac7eb 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js @@ -1,9 +1,11 @@ /** * WordPress dependencies */ -import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; +import { + __experimentalItemGroup as ItemGroup, + __experimentalItem as Item, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { useViewportMatch } from '@wordpress/compose'; @@ -14,24 +16,8 @@ import { useViewportMatch } from '@wordpress/compose'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; import { useLink } from '../routes/link'; import SidebarNavigationItem from '../sidebar-navigation-item'; -import { useLocation } from '../routes'; -import { store as editSiteStore } from '../../store'; import AddNewTemplate from '../add-new-template'; -function omit( object, keys ) { - return Object.fromEntries( - Object.entries( object ).filter( ( [ key ] ) => ! keys.includes( key ) ) - ); -} - -const Item = ( { item } ) => { - const linkInfo = useLink( item.params ); - const props = item.params - ? { ...omit( item, 'params' ), ...linkInfo } - : item; - return ; -}; - const config = { wp_template: { path: '/templates', @@ -53,23 +39,20 @@ const config = { }, }; +const TemplateItem = ( { postType, postId, ...props } ) => { + const linkInfo = useLink( { + postType, + postId, + path: config[ postType ].path + '/single', + } ); + return ; +}; + export default function SidebarNavigationScreenTemplates( { postType = 'wp_template', } ) { - const { params } = useLocation(); const isMobileViewport = useViewportMatch( 'medium', '<' ); - // Ideally the URL params would be enough. - // Loading the editor should ideally redirect to the home page - // instead of fetching the edited entity here. - const { editedPostId, editedPostType } = useSelect( ( select ) => { - const { getEditedPostType, getEditedPostId } = select( editSiteStore ); - return { - editedPostId: getEditedPostId(), - editedPostType: getEditedPostType(), - }; - }, [] ); - const { records: templates, isResolving: isLoading } = useEntityRecords( 'postType', postType, @@ -78,44 +61,15 @@ export default function SidebarNavigationScreenTemplates( { } ); - let items = []; - if ( isLoading ) { - items = [ - { - children: config[ postType ].labels.loading, - }, - ]; - } else if ( ! templates && ! isLoading ) { - items = [ - { - children: config[ postType ].labels.notFound, - }, - ]; - } else { - items = templates?.map( ( template ) => ( { - params: { - postType, - postId: template.id, - }, - children: decodeEntities( - template.title?.rendered || template.slug - ), - 'aria-current': - ( params.postType === postType && - params.postId === template.id ) || - // This is a special case for the home page. - ( editedPostId === template.id && - editedPostType === postType && - !! params.postId ) - ? 'page' - : undefined, - } ) ); - } + const browseAllLink = useLink( { + postType, + postId: undefined, + path: config[ postType ].path + '/all', + } ); return ( - - { items.map( ( item, index ) => ( - - ) ) } - - - + { isLoading && config[ postType ].labels.loading } + { ! isLoading && ( + + { ! templates?.length && ( + + { config[ postType ].labels.notFound } + + ) } + { ( templates ?? [] ).map( ( template ) => ( + + { decodeEntities( + template.title?.rendered || + template.slug + ) } + + ) ) } + { ! isMobileViewport && ( + + ) } + + ) } } /> diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/index.js b/packages/edit-site/src/components/sidebar-navigation-screen/index.js index a5f2eb2cc927b..754508985be06 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen/index.js @@ -4,19 +4,33 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, - __experimentalNavigatorBackButton as NavigatorBackButton, + __experimentalNavigatorToParentButton as NavigatorToParentButton, + Button, __experimentalNavigatorScreen as NavigatorScreen, } from '@wordpress/components'; -import { isRTL, __, sprintf } from '@wordpress/i18n'; +import { isRTL, __ } from '@wordpress/i18n'; import { chevronRight, chevronLeft } from '@wordpress/icons'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../store'; +import { unlock } from '../../private-apis'; export default function SidebarNavigationScreen( { path, - parentTitle, title, actions, content, } ) { + const { dashboardLink } = useSelect( ( select ) => { + const { getSettings } = unlock( select( editSiteStore ) ); + return { + dashboardLink: getSettings().__experimentalDashboardLink, + }; + }, [] ); + return ( - { parentTitle ? ( - ) : ( -
    + - - - { showLabels && ( - -
    - { getTitle() } -
    -
    - { entityConfig?.label } -
    -
    - ) } - - - { showEditButton && ( - ) } + - { isMobileViewport && ! isMobileCanvasVisible && ( - - ) } - - ); - } -); + { showLabels &&
    { siteTitle }
    } + + + ); +} ); export default SiteHub; diff --git a/packages/edit-site/src/components/site-hub/style.scss b/packages/edit-site/src/components/site-hub/style.scss index dc7d121e7caf1..004bf19251422 100644 --- a/packages/edit-site/src/components/site-hub/style.scss +++ b/packages/edit-site/src/components/site-hub/style.scss @@ -5,11 +5,6 @@ gap: $grid-unit-10; } -.edit-site-site-hub__edit-button { - height: $grid-unit-40; - color: $white; -} - .edit-site-site-hub__post-type { opacity: 0.6; } @@ -18,12 +13,7 @@ height: $header-height; width: $header-height + 4px; flex-shrink: 0; -} - -.edit-site-layout.is-edit-mode { - .edit-site-site-hub__view-mode-toggle-container { - width: $header-height; - } + background: $gray-900; } .edit-site-site-hub__text-content { diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index dfeb1d419c934..b4f55ad1de271 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -21,7 +21,7 @@ import { } from '@wordpress/blocks'; import { BlockPreview, - experiments as blockEditorExperiments, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { closeSmall } from '@wordpress/icons'; import { useResizeObserver } from '@wordpress/compose'; @@ -32,7 +32,7 @@ import { useMemo, memo } from '@wordpress/element'; */ import { unlock } from '../../private-apis'; -const { useGlobalStyle } = unlock( blockEditorExperiments ); +const { useGlobalStyle } = unlock( blockEditorPrivateApis ); const SLOT_FILL_NAME = 'EditSiteStyleBook'; const { Slot: StyleBookSlot, Fill: StyleBookFill } = diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js index a6061d205ffc0..52fae37e0d51d 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js @@ -2,7 +2,8 @@ * WordPress dependencies */ import { useEffect } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreDataStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -11,22 +12,55 @@ import { useLocation } from '../routes'; import { store as editSiteStore } from '../../store'; export default function useInitEditedEntityFromURL() { + const { params: { postId, postType, path = '/' } = {} } = useLocation(); + const { isRequestingSite, homepageId } = useSelect( ( select ) => { + const { getSite } = select( coreDataStore ); + const siteData = getSite(); + + return { + isRequestingSite: ! siteData, + homepageId: + siteData?.show_on_front === 'page' + ? siteData.page_on_front + : null, + }; + }, [] ); + const { setTemplate, setTemplatePart, setPage } = useDispatch( editSiteStore ); - const { - params: { postId, postType }, - } = useLocation(); - // Set correct entity on page navigation. useEffect( () => { - // This URL scheme mean we can't open a template part with the context of a given post. - // Potentially posts and pages could be moved to a "context" query string instead. - if ( 'page' === postType || 'post' === postType ) { - setPage( { context: { postType, postId } } ); // Resolves correct template based on ID. - } else if ( 'wp_template' === postType ) { - setTemplate( postId ); - } else if ( 'wp_template_part' === postType ) { - setTemplatePart( postId ); + switch ( path ) { + case '/templates/single': + setTemplate( postId ); + break; + case '/template-parts/single': + setTemplatePart( postId ); + break; + case '/navigation/single': + setPage( { + context: { postType, postId }, + } ); + break; + default: { + if ( homepageId ) { + setPage( { + context: { postType: 'page', postId: homepageId }, + } ); + } else if ( ! isRequestingSite ) { + setPage( { + path: '/', + } ); + } + } } - }, [ postId, postType ] ); + }, [ + path, + postId, + homepageId, + isRequestingSite, + setPage, + setTemplate, + setTemplatePart, + ] ); } diff --git a/packages/edit-site/src/components/sync-state-with-url/use-sync-sidebar-path-with-url.js b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js similarity index 62% rename from packages/edit-site/src/components/sync-state-with-url/use-sync-sidebar-path-with-url.js rename to packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js index 885772cdf4046..f7376fcffbe19 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-sync-sidebar-path-with-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js @@ -9,28 +9,28 @@ import { useEffect, useRef } from '@wordpress/element'; */ import { useLocation, useHistory } from '../routes'; -export default function useSyncSidebarPathWithURL() { +export default function useSyncPathWithURL() { const history = useHistory(); const { params } = useLocation(); - const { sidebar = '/' } = params; + const { path = '/' } = params; const { location, goTo } = useNavigator(); - const currentSidebar = useRef( sidebar ); + const currentPath = useRef( path ); const currentNavigatorLocation = useRef( location.path ); useEffect( () => { - currentSidebar.current = sidebar; - if ( sidebar !== currentNavigatorLocation.current ) { - goTo( sidebar ); + currentPath.current = path; + if ( path !== currentNavigatorLocation.current ) { + goTo( path ); } - }, [ sidebar ] ); + }, [ path ] ); useEffect( () => { currentNavigatorLocation.current = location.path; - if ( location.path !== currentSidebar.current ) { + if ( location.path !== currentPath.current ) { history.push( { ...params, - sidebar: location.path, + path: location.path, } ); } }, [ location.path, history ] ); - return sidebar; + return path; } diff --git a/packages/edit-site/src/components/use-edited-entity-record/index.js b/packages/edit-site/src/components/use-edited-entity-record/index.js index 5d72877095782..66377e0d80976 100644 --- a/packages/edit-site/src/components/use-edited-entity-record/index.js +++ b/packages/edit-site/src/components/use-edited-entity-record/index.js @@ -12,7 +12,7 @@ import { decodeEntities } from '@wordpress/html-entities'; import { store as editSiteStore } from '../../store'; export default function useEditedEntityRecord() { - const { record, title, isLoaded } = useSelect( ( select ) => { + const { record, title, description, isLoaded } = useSelect( ( select ) => { const { getEditedPostType, getEditedPostId } = select( editSiteStore ); const { getEditedEntityRecord } = select( coreStore ); const { __experimentalGetTemplateInfo: getTemplateInfo } = @@ -21,10 +21,12 @@ export default function useEditedEntityRecord() { const postId = getEditedPostId(); const _record = getEditedEntityRecord( 'postType', postType, postId ); const _isLoaded = !! postId; + const templateInfo = getTemplateInfo( _record ); return { record: _record, - title: getTemplateInfo( _record ).title, + title: templateInfo.title, + description: templateInfo.description, isLoaded: _isLoaded, }; }, [] ); @@ -33,5 +35,7 @@ export default function useEditedEntityRecord() { isLoaded, record, getTitle: () => ( title ? decodeEntities( title ) : null ), + getDescription: () => + description ? decodeEntities( description ) : null, }; } diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js index 24b71c8aaffaf..815aa6f6b20c3 100644 --- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js +++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js @@ -11,7 +11,7 @@ import { createHigherOrderComponent } from '@wordpress/compose'; import { InspectorAdvancedControls, store as blockEditorStore, - experiments as blockEditorExperiments, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { BaseControl, Button } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; @@ -29,7 +29,7 @@ import { store as noticesStore } from '@wordpress/notices'; import { useSupportedStyles } from '../../components/global-styles/hooks'; import { unlock } from '../../private-apis'; -const { GlobalStylesContext } = unlock( blockEditorExperiments ); +const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); // TODO: Temporary duplication of constant in @wordpress/block-editor. Can be // removed by moving PushChangesToGlobalStylesControl to diff --git a/packages/edit-site/src/utils/get-is-list-page.js b/packages/edit-site/src/utils/get-is-list-page.js index ef08058d00e82..58a4ebe0bfdf4 100644 --- a/packages/edit-site/src/utils/get-is-list-page.js +++ b/packages/edit-site/src/utils/get-is-list-page.js @@ -1,11 +1,11 @@ /** * Returns if the params match the list page route. * - * @param {Object} params The search params. - * @param {string} params.postId The post ID. - * @param {string} params.postType The post type. + * @param {Object} params The url params. + * @param {string} params.path The current path. + * * @return {boolean} Is list page or not. */ -export default function getIsListPage( { postId, postType } ) { - return !! ( ! postId && postType ); +export default function getIsListPage( { path } ) { + return path === '/templates/all' || path === '/template-parts/all'; } diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index 42d8117c14234..042cd0dc9c617 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -13,7 +13,7 @@ import { useMemo } from '@wordpress/element'; import { BlockEditorKeyboardShortcuts, CopyHandler, - experiments as blockEditorExperiments, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { ReusableBlocksMenuItems } from '@wordpress/reusable-blocks'; import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; @@ -29,7 +29,7 @@ import { store as editWidgetsStore } from '../../store'; import { ALLOW_REUSABLE_BLOCKS } from '../../constants'; import { unlock } from '../../private-apis'; -const { ExperimentalBlockEditorProvider } = unlock( blockEditorExperiments ); +const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); export default function WidgetAreasBlockEditorProvider( { blockEditorSettings, diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index eaf9b1bf7d19d..1cff19c7daae7 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -8,7 +8,7 @@ import { EntityProvider, useEntityBlockEditor } from '@wordpress/core-data'; import { BlockEditorProvider, BlockContextProvider, - experiments as blockEditorExperiments, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { ReusableBlocksMenuItems } from '@wordpress/reusable-blocks'; import { store as noticesStore } from '@wordpress/notices'; @@ -21,7 +21,7 @@ import { store as editorStore } from '../../store'; import useBlockEditorSettings from './use-block-editor-settings'; import { unlock } from '../../lockUnlock'; -const { ExperimentalBlockEditorProvider } = unlock( blockEditorExperiments ); +const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); export const ExperimentalEditorProvider = withRegistryProvider( ( { diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 8b9d9e5ba86d7..4c5c431e5915f 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -73,6 +73,7 @@ const BLOCK_EDITOR_SETTINGS = [ '__unstableHasCustomAppender', '__unstableIsPreviewMode', '__unstableResolvedAssets', + '__unstableIsBlockBasedTheme', ]; /** diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index c54c895d961a0..edfc3045f1100 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -4,7 +4,7 @@ import { ExperimentalEditorProvider } from './components/provider'; import { lock } from './lockUnlock'; -export const experiments = {}; -lock( experiments, { +export const privateApis = {}; +lock( privateApis, { ExperimentalEditorProvider, } ); diff --git a/packages/format-library/src/code/index.js b/packages/format-library/src/code/index.js index f3651504d57ce..27a99ebc989e2 100644 --- a/packages/format-library/src/code/index.js +++ b/packages/format-library/src/code/index.js @@ -20,16 +20,18 @@ export const code = { __unstableInputRule( value ) { const BACKTICK = '`'; const { start, text } = value; - const characterBefore = text.slice( start - 1, start ); + const characterBefore = text[ start - 1 ]; // Quick check the text for the necessary character. if ( characterBefore !== BACKTICK ) { return value; } - const textBefore = text.slice( 0, start - 1 ); - const indexBefore = textBefore.lastIndexOf( BACKTICK ); + if ( start - 2 < 0 ) { + return value; + } + const indexBefore = text.lastIndexOf( BACKTICK, start - 2 ); if ( indexBefore === -1 ) { return value; } diff --git a/packages/interface/src/components/complementary-area/index.js b/packages/interface/src/components/complementary-area/index.js index bac359b10326a..5617c39da301b 100644 --- a/packages/interface/src/components/complementary-area/index.js +++ b/packages/interface/src/components/complementary-area/index.js @@ -47,26 +47,29 @@ function useAdjustComplementaryListener( const { enableComplementaryArea, disableComplementaryArea } = useDispatch( interfaceStore ); useEffect( () => { - // If the complementary area is active and the editor is switching from a big to a small window size. + // If the complementary area is active and the editor is switching from + // a big to a small window size. if ( isActive && isSmall && ! previousIsSmall.current ) { - // Disable the complementary area. disableComplementaryArea( scope ); - // Flag the complementary area to be reopened when the window size goes from small to big. + // Flag the complementary area to be reopened when the window size + // goes from small to big. shouldOpenWhenNotSmall.current = true; } else if ( - // If there is a flag indicating the complementary area should be enabled when we go from small to big window size - // and we are going from a small to big window size. + // If there is a flag indicating the complementary area should be + // enabled when we go from small to big window size and we are going + // from a small to big window size. shouldOpenWhenNotSmall.current && ! isSmall && previousIsSmall.current ) { - // Remove the flag indicating the complementary area should be enabled. + // Remove the flag indicating the complementary area should be + // enabled. shouldOpenWhenNotSmall.current = false; - // Enable the complementary area. enableComplementaryArea( scope, identifier ); } else if ( - // If the flag is indicating the current complementary should be reopened but another complementary area becomes active, - // remove the flag. + // If the flag is indicating the current complementary should be + // reopened but another complementary area becomes active, remove + // the flag. shouldOpenWhenNotSmall.current && activeArea && activeArea !== identifier @@ -97,21 +100,29 @@ function ComplementaryArea( { isActiveByDefault, showIconLabels = false, } ) { - const { isActive, isPinned, activeArea, isSmall, isLarge } = useSelect( - ( select ) => { - const { getActiveComplementaryArea, isItemPinned } = - select( interfaceStore ); - const _activeArea = getActiveComplementaryArea( scope ); - return { - isActive: _activeArea === identifier, - isPinned: isItemPinned( scope, identifier ), - activeArea: _activeArea, - isSmall: select( viewportStore ).isViewportMatch( '< medium' ), - isLarge: select( viewportStore ).isViewportMatch( 'large' ), - }; - }, - [ identifier, scope ] - ); + const { isLoading, isActive, isPinned, activeArea, isSmall, isLarge } = + useSelect( + ( select ) => { + const { + getActiveComplementaryArea, + isComplementaryAreaLoading, + isItemPinned, + } = select( interfaceStore ); + + const _activeArea = getActiveComplementaryArea( scope ); + + return { + isLoading: isComplementaryAreaLoading( scope ), + isActive: _activeArea === identifier, + isPinned: isItemPinned( scope, identifier ), + activeArea: _activeArea, + isSmall: + select( viewportStore ).isViewportMatch( '< medium' ), + isLarge: select( viewportStore ).isViewportMatch( 'large' ), + }; + }, + [ identifier, scope ] + ); useAdjustComplementaryListener( scope, identifier, @@ -127,8 +138,12 @@ function ComplementaryArea( { } = useDispatch( interfaceStore ); useEffect( () => { + // Set initial visibility: For large screens, enable if it's active by + // default. For small screens, always initially disable. if ( isActiveByDefault && activeArea === undefined && ! isSmall ) { enableComplementaryArea( scope, identifier ); + } else if ( activeArea === undefined && isSmall ) { + disableComplementaryArea( scope, identifier ); } }, [ activeArea, isActiveByDefault, scope, identifier, isSmall ] ); @@ -144,6 +159,7 @@ function ComplementaryArea( { isActive && ( ! showIconLabels || isLarge ) } aria-expanded={ isActive } + aria-disabled={ isLoading } label={ title } icon={ showIconLabels ? check : icon } showTooltip={ ! showIconLabels } diff --git a/packages/interface/src/store/selectors.js b/packages/interface/src/store/selectors.js index 13b5915e17aac..c92e45bbd3c59 100644 --- a/packages/interface/src/store/selectors.js +++ b/packages/interface/src/store/selectors.js @@ -28,7 +28,7 @@ export const getActiveComplementaryArea = createRegistrySelector( } // Return `null` to indicate the user hid the complementary area. - if ( ! isComplementaryAreaVisible ) { + if ( isComplementaryAreaVisible === false ) { return null; } @@ -36,6 +36,18 @@ export const getActiveComplementaryArea = createRegistrySelector( } ); +export const isComplementaryAreaLoading = createRegistrySelector( + ( select ) => ( state, scope ) => { + const isVisible = select( preferencesStore ).get( + scope, + 'isComplementaryAreaVisible' + ); + const identifier = state?.complementaryAreas?.[ scope ]; + + return isVisible && identifier === undefined; + } +); + /** * Returns a boolean indicating if an item is pinned or not. * diff --git a/packages/keycodes/src/index.js b/packages/keycodes/src/index.js index 2aa1cb70fa3f3..cd31fef5fa719 100644 --- a/packages/keycodes/src/index.js +++ b/packages/keycodes/src/index.js @@ -390,6 +390,14 @@ export const isKeyboardEvent = mapValues( ) => { const mods = getModifiers( _isApple ); const eventMods = getEventModifiers( event ); + /** @type {Record} */ + const replacementWithShiftKeyMap = { + Comma: ',', + Backslash: '\\', + // Windows returns `\` for both IntlRo and IntlYen. + IntlRo: '\\', + IntlYen: '\\', + }; const modsDiff = mods.filter( ( mod ) => ! eventMods.includes( mod ) @@ -412,16 +420,17 @@ export const isKeyboardEvent = mapValues( key = String.fromCharCode( event.keyCode ).toLowerCase(); } - // Replace some characters to match the key indicated - // by the shortcut on Windows. - if ( ! _isApple() ) { - if ( - event.shiftKey && - character.length === 1 && - event.code === 'Comma' - ) { - key = ','; - } + // `event.key` returns the value of the key pressed, taking into the state of + // modifier keys such as `Shift`. If the shift key is pressed, a different + // value may be returned depending on the keyboard layout. It is necessary to + // convert to the physical key value that don't take into account keyboard + // layout or modifier key state. + if ( + event.shiftKey && + character.length === 1 && + replacementWithShiftKeyMap[ event.code ] + ) { + key = replacementWithShiftKeyMap[ event.code ]; } // For backwards compatibility. diff --git a/packages/private-apis/README.md b/packages/private-apis/README.md index a57e5e339e4be..828663dc760de 100644 --- a/packages/private-apis/README.md +++ b/packages/private-apis/README.md @@ -58,17 +58,17 @@ Use `lock()` and `unlock()` to privately distribute the `__experimental` APIs ac // In packages/package1/index.js: import { lock } from './private-apis'; -export const experiments = {}; +export const privateApis = {}; /* Attach private data to the exported object */ -lock( experiments, { +lock( privateApis, { __experimentalFunction: function () {}, } ); // In packages/package2/index.js: -import { experiments } from '@wordpress/package1'; +import { privateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; -const { __experimentalFunction } = unlock( experiments ); +const { __experimentalFunction } = unlock( privateApis ); ``` ## Shipping experimental APIs diff --git a/packages/private-apis/src/implementation.js b/packages/private-apis/src/implementation.js index 26502a4723111..6a88e9812ca8c 100644 --- a/packages/private-apis/src/implementation.js +++ b/packages/private-apis/src/implementation.js @@ -1,15 +1,15 @@ /** - * wordpress/experimental – the utilities to enable private cross-package - * exports of experimental APIs. + * wordpress/private-apis – the utilities to enable private cross-package + * exports of private APIs. * * This "implementation.js" file is needed for the sake of the unit tests. It * exports more than the public API of the package to aid in testing. */ /** - * The list of core modules allowed to opt-in to the experimental APIs. + * The list of core modules allowed to opt-in to the private APIs. */ -const CORE_MODULES_USING_EXPERIMENTS = [ +const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/block-editor', '@wordpress/block-library', '@wordpress/blocks', @@ -24,21 +24,21 @@ const CORE_MODULES_USING_EXPERIMENTS = [ /** * A list of core modules that already opted-in to - * the experiments package. + * the privateApis package. * * @type {string[]} */ -const registeredExperiments = []; +const registeredPrivateApis = []; /* * Warning for theme and plugin developers. * - * The use of experimental developer APIs is intended for use by WordPress Core + * The use of private developer APIs is intended for use by WordPress Core * and the Gutenberg plugin exclusively. * * Dangerously opting in to using these APIs is NOT RECOMMENDED. Furthermore, * the WordPress Core philosophy to strive to maintain backward compatibility - * for third-party developers DOES NOT APPLY to experimental APIs. + * for third-party developers DOES NOT APPLY to private APIs. * * THE CONSENT STRING FOR OPTING IN TO THESE APIS MAY CHANGE AT ANY TIME AND * WITHOUT NOTICE. THIS CHANGE WILL BREAK EXISTING THIRD-PARTY CODE. SUCH A @@ -59,7 +59,7 @@ try { /** * Called by a @wordpress package wishing to opt-in to accessing or exposing - * private experimental APIs. + * private private APIs. * * @param {string} consent The consent string. * @param {string} moduleName The name of the module that is opting in. @@ -69,7 +69,7 @@ export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( consent, moduleName ) => { - if ( ! CORE_MODULES_USING_EXPERIMENTS.includes( moduleName ) ) { + if ( ! CORE_MODULES_USING_PRIVATE_APIS.includes( moduleName ) ) { throw new Error( `You tried to opt-in to unstable APIs as module "${ moduleName }". ` + 'This feature is only for JavaScript modules shipped with WordPress core. ' + @@ -80,7 +80,7 @@ export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( } if ( ! allowReRegistration && - registeredExperiments.includes( moduleName ) + registeredPrivateApis.includes( moduleName ) ) { // This check doesn't play well with Story Books / Hot Module Reloading // and isn't included in the Gutenberg plugin. It only matters in the @@ -102,7 +102,7 @@ export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( 'your product will inevitably break on the next WordPress release.' ); } - registeredExperiments.push( moduleName ); + registeredPrivateApis.push( moduleName ); return { lock, @@ -138,10 +138,10 @@ function lock( object, privateData ) { if ( ! object ) { throw new Error( 'Cannot lock an undefined object.' ); } - if ( ! ( __experiment in object ) ) { - object[ __experiment ] = {}; + if ( ! ( __private in object ) ) { + object[ __private ] = {}; } - lockedData.set( object[ __experiment ], privateData ); + lockedData.set( object[ __private ], privateData ); } /** @@ -171,13 +171,13 @@ function unlock( object ) { if ( ! object ) { throw new Error( 'Cannot unlock an undefined object.' ); } - if ( ! ( __experiment in object ) ) { + if ( ! ( __private in object ) ) { throw new Error( 'Cannot unlock an object that was not locked before. ' ); } - return lockedData.get( object[ __experiment ] ); + return lockedData.get( object[ __private ] ); } const lockedData = new WeakMap(); @@ -186,18 +186,18 @@ const lockedData = new WeakMap(); * Used by lock() and unlock() to uniquely identify the private data * related to a containing object. */ -const __experiment = Symbol( 'Experiment ID' ); +const __private = Symbol( 'Private API ID' ); // Unit tests utilities: /** * Private function to allow the unit tests to allow - * a mock module to access the experimental APIs. + * a mock module to access the private APIs. * * @param {string} name The name of the module. */ export function allowCoreModule( name ) { - CORE_MODULES_USING_EXPERIMENTS.push( name ); + CORE_MODULES_USING_PRIVATE_APIS.push( name ); } /** @@ -205,16 +205,16 @@ export function allowCoreModule( name ) { * a custom list of allowed modules. */ export function resetAllowedCoreModules() { - while ( CORE_MODULES_USING_EXPERIMENTS.length ) { - CORE_MODULES_USING_EXPERIMENTS.pop(); + while ( CORE_MODULES_USING_PRIVATE_APIS.length ) { + CORE_MODULES_USING_PRIVATE_APIS.pop(); } } /** * Private function to allow the unit tests to reset - * the list of registered experiments. + * the list of registered private apis. */ -export function resetRegisteredExperiments() { - while ( registeredExperiments.length ) { - registeredExperiments.pop(); +export function resetRegisteredPrivateApis() { + while ( registeredPrivateApis.length ) { + registeredPrivateApis.pop(); } } diff --git a/packages/private-apis/src/test/index.js b/packages/private-apis/src/test/index.js index 2456d5ca5039d..2e73a1a58eaa1 100644 --- a/packages/private-apis/src/test/index.js +++ b/packages/private-apis/src/test/index.js @@ -3,16 +3,16 @@ */ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '../'; import { - resetRegisteredExperiments, + resetRegisteredPrivateApis, resetAllowedCoreModules, allowCoreModule, } from '../implementation'; beforeEach( () => { - resetRegisteredExperiments(); + resetRegisteredPrivateApis(); resetAllowedCoreModules(); - allowCoreModule( '@experiments/test' ); - allowCoreModule( '@experiments/test-consumer' ); + allowCoreModule( '@privateApis/test' ); + allowCoreModule( '@privateApis/test-consumer' ); } ); const requiredConsent = @@ -23,7 +23,7 @@ describe( '__dangerousOptInToUnstableAPIsOnlyForCoreModules', () => { expect( () => { __dangerousOptInToUnstableAPIsOnlyForCoreModules( '', - '@experiments/test' + '@privateApis/test' ); } ).toThrow( /without confirming you know the consequences/ ); } ); @@ -41,18 +41,18 @@ describe( '__dangerousOptInToUnstableAPIsOnlyForCoreModules', () => { expect( () => { __dangerousOptInToUnstableAPIsOnlyForCoreModules( requiredConsent, - '@experiments/test' + '@privateApis/test' ); __dangerousOptInToUnstableAPIsOnlyForCoreModules( requiredConsent, - '@experiments/test' + '@privateApis/test' ); } ).toThrow( /is already registered/ ); } ); it( 'Should grant access to unstable APIs when passed both a consent string and a previously unregistered package name', () => { const unstableAPIs = __dangerousOptInToUnstableAPIsOnlyForCoreModules( requiredConsent, - '@experiments/test' + '@privateApis/test' ); expect( unstableAPIs.lock ).toEqual( expect.any( Function ) ); expect( unstableAPIs.unlock ).toEqual( expect.any( Function ) ); @@ -62,14 +62,14 @@ describe( '__dangerousOptInToUnstableAPIsOnlyForCoreModules', () => { describe( 'lock(), unlock()', () => { let lock, unlock; beforeEach( () => { - // This would live in @experiments/test: - // Opt-in to experimental APIs - const experimentsAPI = __dangerousOptInToUnstableAPIsOnlyForCoreModules( + // This would live in @privateApis/test: + // Opt-in to private APIs + const privateApisAPI = __dangerousOptInToUnstableAPIsOnlyForCoreModules( requiredConsent, - '@experiments/test' + '@privateApis/test' ); - lock = experimentsAPI.lock; - unlock = experimentsAPI.unlock; + lock = privateApisAPI.lock; + unlock = privateApisAPI.unlock; } ); it( 'Should lock and unlock objects "inside" other objects', () => { @@ -120,95 +120,95 @@ describe( 'lock(), unlock()', () => { lock( object, privateData ); // This would live in @wordpress/core-data: - // Register the experimental APIs - const coreDataExperiments = + // Register the private APIs + const coreDataPrivateApis = __dangerousOptInToUnstableAPIsOnlyForCoreModules( requiredConsent, - '@experiments/test-consumer' + '@privateApis/test-consumer' ); - // Get the experimental APIs registered by @experiments/test - expect( coreDataExperiments.unlock( object ).secret ).toBe( 'sh' ); + // Get the private APIs registered by @privateApis/test + expect( coreDataPrivateApis.unlock( object ).secret ).toBe( 'sh' ); } ); } ); describe( 'Specific use-cases of sharing private APIs', () => { let lock, unlock; beforeEach( () => { - // This would live in @experiments/test: - // Opt-in to experimental APIs - const experimentsAPI = __dangerousOptInToUnstableAPIsOnlyForCoreModules( + // This would live in @privateApis/test: + // Opt-in to private APIs + const privateApisAPI = __dangerousOptInToUnstableAPIsOnlyForCoreModules( requiredConsent, - '@experiments/test' + '@privateApis/test' ); - lock = experimentsAPI.lock; - unlock = experimentsAPI.unlock; + lock = privateApisAPI.lock; + unlock = privateApisAPI.unlock; } ); - it( 'Should enable privately exporting experimental functions', () => { + it( 'Should enable privately exporting private functions', () => { /** - * Problem: The private __experimentalFunction should not be publicly + * Problem: The private __privateFunction should not be publicly * exposed to the consumers of package1. */ - function __experimentalFunction() {} + function __privateFunction() {} /** * Solution: Privately lock it inside a publicly exported object. * * In `package1/index.js` we'd say: * * ```js - * export const experiments = {}; - * lock(experiments, { - * __experimentalFunction + * export const privateApis = {}; + * lock(privateApis, { + * __privateFunction * }); * ``` * * Let's simulate in the test code: */ - const experiments = {}; + const privateApis = {}; const package1Exports = { - experiments, + privateApis, }; - lock( experiments, { __experimentalFunction } ); + lock( privateApis, { __privateFunction } ); /** * Then, in the consumer package we'd say: * * ```js - * import { experiments } from 'package1'; - * const { __experimentalFunction } = unlock( experiments ); + * import { privateApis } from 'package1'; + * const { __privateFunction } = unlock( privateApis ); * ``` * * Let's simulate that, too: */ const unlockedFunction = unlock( - package1Exports.experiments - ).__experimentalFunction; - expect( unlockedFunction ).toBe( __experimentalFunction ); + package1Exports.privateApis + ).__privateFunction; + expect( unlockedFunction ).toBe( __privateFunction ); } ); - it( 'Should enable exporting functions with private experimental arguments', () => { + it( 'Should enable exporting functions with private private arguments', () => { /** - * The publicly exported function does not have any experimental + * The publicly exported function does not have any private * arguments. * * @param {any} data The data to log */ function logData( data ) { - // Internally, it calls the experimental version of the function - // with fixed default values for the experimental arguments. - __experimentalLogData( data, 'plain' ); + // Internally, it calls the private version of the function + // with fixed default values for the private arguments. + __privateLogData( data, 'plain' ); } /** - * The private experimental function is not publicly exported. Instead, it's + * The private private function is not publicly exported. Instead, it's * "locked" inside of the public logData function. It can be unlocked by any * participant of the private importing system. * - * @param {any} data The data to log - * @param {string} __experimentalFormat The logging format to use. + * @param {any} data The data to log + * @param {string} __privateFormat The logging format to use. */ - function __experimentalLogData( data, __experimentalFormat ) { - if ( __experimentalFormat === 'table' ) { + function __privateLogData( data, __privateFormat ) { + if ( __privateFormat === 'table' ) { // eslint-disable-next-line no-console console.table( data ); } else { @@ -216,12 +216,12 @@ describe( 'Specific use-cases of sharing private APIs', () => { console.log( data ); } } - lock( logData, __experimentalLogData ); + lock( logData, __privateLogData ); /** * In package/log-data.js: * * ```js - * lock( logData, __experimentalLogData ); + * lock( logData, __privateLogData ); * export logData; * ``` * @@ -232,43 +232,40 @@ describe( 'Specific use-cases of sharing private APIs', () => { * ``` * * And that's it! The public function is publicly exported, and the - * experimental function is available via unlock( logData ): + * private function is available via unlock( logData ): * * ```js * import { logData } from 'package1'; * const experimenalLogData = unlock( logData ); * ``` */ - expect( unlock( logData ) ).toBe( __experimentalLogData ); + expect( unlock( logData ) ).toBe( __privateLogData ); } ); - it( 'Should enable exporting React Components with private experimental properties', () => { + it( 'Should enable exporting React Components with private private properties', () => { // eslint-disable-next-line jsdoc/require-param /** - * The publicly exported component does not have any experimental + * The publicly exported component does not have any private * properties. */ function DataTable( { data } ) { - // Internally, it calls the experimental version of the function - // with fixed default values for the experimental arguments. + // Internally, it calls the private version of the function + // with fixed default values for the private arguments. return ( - ); } // eslint-disable-next-line jsdoc/require-param /** - * The private experimental component is not publicly exported. Instead, it's + * The private private component is not publicly exported. Instead, it's * "locked" inside of the public logData function. It can be unlocked by any * participant of the private importing system. */ - function ExperimentalDataTable( { - data, - __experimentalFancyFormatting, - } ) { - const className = __experimentalFancyFormatting + function PrivateDataTable( { data, __privateFancyFormatting } ) { + const className = __privateFancyFormatting ? 'savage-css' : 'regular-css'; return ( @@ -283,12 +280,12 @@ describe( 'Specific use-cases of sharing private APIs', () => { ); } - lock( DataTable, ExperimentalDataTable ); + lock( DataTable, PrivateDataTable ); /** * In package/data-table.js: * * ```js - * lock( DataTable, ExperimentalDataTable ); + * lock( DataTable, PrivateDataTable ); * export DataTable; * ``` * @@ -299,13 +296,13 @@ describe( 'Specific use-cases of sharing private APIs', () => { * ``` * * And that's it! The public function is publicly exported, and the - * experimental function is available via unlock( logData ): + * private function is available via unlock( logData ): * * ```js * import { DataTable } from 'package1'; - * const ExperimentalDataTable = unlock( DataTable ); + * const PrivateDataTable = unlock( DataTable ); * ``` */ - expect( unlock( DataTable ) ).toBe( ExperimentalDataTable ); + expect( unlock( DataTable ) ).toBe( PrivateDataTable ); } ); } ); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt index 3e880011cd31f..4e97a3974d614 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt @@ -8,6 +8,7 @@ data class GutenbergProps @JvmOverloads constructor( val enableContactInfoBlock: Boolean, val enableLayoutGridBlock: Boolean, val enableTiledGalleryBlock: Boolean, + val enableVideoPressBlock: Boolean, val enableFacebookEmbed: Boolean, val enableInstagramEmbed: Boolean, val enableLoomEmbed: Boolean, @@ -68,6 +69,7 @@ data class GutenbergProps @JvmOverloads constructor( putBoolean(PROP_CAPABILITIES_CONTACT_INFO_BLOCK, enableContactInfoBlock) putBoolean(PROP_CAPABILITIES_LAYOUT_GRID_BLOCK, enableLayoutGridBlock) putBoolean(PROP_CAPABILITIES_TILED_GALLERY_BLOCK, enableTiledGalleryBlock) + putBoolean(PROP_CAPABILITIES_VIDEOPRESS_BLOCK, enableVideoPressBlock) putBoolean(PROP_CAPABILITIES_MEDIAFILES_COLLECTION_BLOCK, enableMediaFilesCollectionBlocks) putBoolean(PROP_CAPABILITIES_UNSUPPORTED_BLOCK_EDITOR, enableUnsupportedBlockEditor) putBoolean(PROP_CAPABILITIES_CAN_ENABLE_UNSUPPORTED_BLOCK_EDITOR, canEnableUnsupportedBlockEditor) @@ -111,6 +113,7 @@ data class GutenbergProps @JvmOverloads constructor( const val PROP_CAPABILITIES_CONTACT_INFO_BLOCK = "contactInfoBlock" const val PROP_CAPABILITIES_LAYOUT_GRID_BLOCK = "layoutGridBlock" const val PROP_CAPABILITIES_TILED_GALLERY_BLOCK = "tiledGalleryBlock" + const val PROP_CAPABILITIES_VIDEOPRESS_BLOCK = "videoPressBlock" const val PROP_CAPABILITIES_FACEBOOK_EMBED_BLOCK = "facebookEmbed" const val PROP_CAPABILITIES_INSTAGRAM_EMBED_BLOCK = "instagramEmbed" const val PROP_CAPABILITIES_LOOM_EMBED_BLOCK = "loomEmbed" diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift index cf41c34ec2c6a..56296d509868d 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift @@ -21,6 +21,7 @@ public enum Capabilities: String { case contactInfoBlock case layoutGridBlock case tiledGalleryBlock + case videoPressBlock case mediaFilesCollectionBlock case mentions case xposts diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java index ae81e808011ea..2c31c3bdc8281 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java @@ -43,6 +43,7 @@ protected Bundle getLaunchOptions() { capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_REUSABLE_BLOCK, false); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_IS_AUDIO_BLOCK_MEDIA_UPLOAD_ENABLED, true); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_TILED_GALLERY_BLOCK, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_VIDEOPRESS_BLOCK, true); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_FACEBOOK_EMBED_BLOCK, true); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_INSTAGRAM_EMBED_BLOCK, true); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_LOOM_EMBED_BLOCK, true); diff --git a/packages/react-native-editor/babel.config.js b/packages/react-native-editor/babel.config.js index 705c9cb820961..62a9959bcf45a 100644 --- a/packages/react-native-editor/babel.config.js +++ b/packages/react-native-editor/babel.config.js @@ -13,6 +13,7 @@ module.exports = function ( api ) { '../../node_modules/@babel/plugin-proposal-async-generator-functions' ), '@babel/plugin-transform-runtime', + '@babel/plugin-transform-named-capturing-groups-regex', [ 'react-native-platform-specific-extensions', { diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index 8d5268e298df1..cb485a6bca720 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -322,6 +322,7 @@ extension GutenbergViewController: GutenbergBridgeDataSource { .canEnableUnsupportedBlockEditor: unsupportedBlockCanBeActivated, .mediaFilesCollectionBlock: true, .tiledGalleryBlock: true, + .videoPressBlock: true, .isAudioBlockMediaUploadEnabled: true, .reusableBlock: false, .facebookEmbed: true, diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 423b8a6ff4154..abb7c26cba866 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -1442,7 +1442,7 @@ public function test_get_styles_for_block_with_padding_aware_alignments() { 'selector' => 'body', ); - $expected = 'body { margin: 0;}.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }body{--wp--style--root--padding-top: 10px;--wp--style--root--padding-right: 12px;--wp--style--root--padding-bottom: 10px;--wp--style--root--padding-left: 12px;}'; + $expected = 'body { margin: 0;}.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding :where(.has-global-padding) { padding-right: 0; padding-left: 0; }.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }.has-global-padding :where(.has-global-padding) > .alignfull { margin-right: 0; margin-left: 0; }.has-global-padding > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }body{--wp--style--root--padding-top: 10px;--wp--style--root--padding-right: 12px;--wp--style--root--padding-bottom: 10px;--wp--style--root--padding-left: 12px;}'; $root_rules = $theme_json->get_root_layout_rules( WP_Theme_JSON::ROOT_BLOCK_SELECTOR, $metadata ); $style_rules = $theme_json->get_styles_for_block( $metadata ); $this->assertEquals( $expected, $root_rules . $style_rules ); diff --git a/schemas/json/block.json b/schemas/json/block.json index 9e7d43470b180..ad3bb45516b9f 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -269,22 +269,22 @@ "properties": { "background": { "type": "boolean", - "description": "This property adds UI controls which allow the user to apply a solid background color to a block.\n\nWhen color support is declared, this property is enabled by default (along with text), so simply setting color will enable background color.\n\nTo disable background support while keeping other color supports enabled, set to false.\n\nWhen the block declares support for color.background, the attributes definition is extended to include two new attributes: backgroundColor and style", + "description": "This property adds UI controls which allow the user to apply a solid background color to a block.\n\nWhen color support is declared, this property is enabled by default (along with text), so simply setting color will enable background color.\n\nTo disable background support while keeping other color supports enabled, set to false.\n\nWhen the block declares support for color.background, its attributes definition is extended to include two new attributes: backgroundColor and style", "default": true }, "gradients": { "type": "boolean", - "description": "This property adds UI controls which allow the user to apply a gradient background to a block.\n\nGradient presets are sourced from editor-gradient-presets theme support.\n\nWhen the block declares support for color.gradient, the attributes definition is extended to include two new attributes: gradient and style", + "description": "This property adds UI controls which allow the user to apply a gradient background to a block.\n\nGradient presets are sourced from editor-gradient-presets theme support.\n\nWhen the block declares support for color.gradient, its attributes definition is extended to include two new attributes: gradient and style", "default": false }, "link": { "type": "boolean", - "description": "This property adds block controls which allow the user to set link color in a block, link color is disabled by default.\n\nLink color presets are sourced from the editor-color-palette theme support.\n\nWhen the block declares support for color.link, the attributes definition is extended to include the style attribute", + "description": "This property adds block controls which allow the user to set link color in a block, link color is disabled by default.\n\nLink color presets are sourced from the editor-color-palette theme support.\n\nWhen the block declares support for color.link, its attributes definition is extended to include the style attribute", "default": false }, "text": { "type": "boolean", - "description": "This property adds block controls which allow the user to set text color in a block.\n\nWhen color support is declared, this property is enabled by default (along with background), so simply setting color will enable text color.\n\nText color presets are sourced from the editor-color-palette theme support.\n\nWhen the block declares support for color.text, the attributes definition is extended to include two new attributes: textColor and style", + "description": "This property adds block controls which allow the user to set text color in a block.\n\nWhen color support is declared, this property is enabled by default (along with background), so simply setting color will enable text color.\n\nText color presets are sourced from the editor-color-palette theme support.\n\nWhen the block declares support for color.text, its attributes definition is extended to include two new attributes: textColor and style", "default": true }, "enableContrastChecker": { @@ -304,6 +304,17 @@ "description": "When the style picker is shown, a dropdown is displayed so the user can select a default style for this block type. If you prefer not to show the dropdown, set this property to false.", "default": true }, + "dimensions": { + "type": "object", + "description": "This value signals that a block supports some of the CSS style properties related to dimensions. When it does, the block editor will show UI controls for the user to set their values if the theme declares support.\n\nWhen the block declares support for a specific dimensions property, its attributes definition is extended to include the style attribute.", + "properties": { + "minHeight": { + "type": "boolean", + "description": "Allow blocks to define a minimum height value.", + "default": false + } + } + }, "html": { "type": "boolean", "description": "By default, a block’s markup can be edited individually. To disable this behavior, set html to false.", @@ -311,7 +322,7 @@ }, "inserter": { "type": "boolean", - "description": "By default, all blocks will appear in the inserter, block transforms menu, Style Book, etc. To hide a block from all parts of the user interface so that it can only be inserted programatically, set inserter to false.", + "description": "By default, all blocks will appear in the inserter, block transforms menu, Style Book, etc. To hide a block from all parts of the user interface so that it can only be inserted programmatically, set inserter to false.", "default": true }, "multiple": { @@ -329,9 +340,20 @@ "description": "A block may want to disable the ability to toggle the lock state. It can be locked/unlocked by a user from the block 'Options' dropdown by default. To disable this behavior, set lock to false.", "default": true }, + "position": { + "type": "object", + "description": "This value signals that a block supports some of the CSS style properties related to position. When it does, the block editor will show UI controls for the user to set their values if the theme declares support.\n\nWhen the block declares support for a specific position property, its attributes definition is extended to include the style attribute.", + "properties": { + "sticky": { + "type": "boolean", + "description": "Allow blocks to stick to their immediate parent when scrolling the page.", + "default": false + } + } + }, "spacing": { "type": "object", - "description": "This value signals that a block supports some of the CSS style properties related to spacing. When it does, the block editor will show UI controls for the user to set their values, if the theme declares support.\n\nWhen the block declares support for a specific spacing property, the attributes definition is extended to include the style attribute.", + "description": "This value signals that a block supports some of the CSS style properties related to spacing. When it does, the block editor will show UI controls for the user to set their values if the theme declares support.\n\nWhen the block declares support for a specific spacing property, its attributes definition is extended to include the style attribute.", "properties": { "margin": { "oneOf": [ @@ -389,16 +411,16 @@ }, "typography": { "type": "object", - "description": "This value signals that a block supports some of the CSS style properties related to typography. When it does, the block editor will show UI controls for the user to set their values, if the theme declares support.\n\nWhen the block declares support for a specific typography property, the attributes definition is extended to include the style attribute.", + "description": "This value signals that a block supports some of the CSS style properties related to typography. When it does, the block editor will show UI controls for the user to set their values if the theme declares support.\n\nWhen the block declares support for a specific typography property, its attributes definition is extended to include the style attribute.", "properties": { "fontSize": { "type": "boolean", - "description": "This value signals that a block supports the font-size CSS style property. When it does, the block editor will show an UI control for the user to set its value.\n\nThe values shown in this control are the ones declared by the theme via the editor-font-sizes theme support, or the default ones if none is provided.\n\nWhen the block declares support for fontSize, the attributes definition is extended to include two new attributes: fontSize and style", + "description": "This value signals that a block supports the font-size CSS style property. When it does, the block editor will show an UI control for the user to set its value.\n\nThe values shown in this control are the ones declared by the theme via the editor-font-sizes theme support, or the default ones if none is provided.\n\nWhen the block declares support for fontSize, its attributes definition is extended to include two new attributes: fontSize and style", "default": false }, "lineHeight": { "type": "boolean", - "description": "This value signals that a block supports the line-height CSS style property. When it does, the block editor will show an UI control for the user to set its value if the theme declares support.\n\nWhen the block declares support for lineHeight, the attributes definition is extended to include a new attribute style of object type with no default assigned. It stores the custom value set by the user. The block can apply a default style by specifying its own style attribute with a default", + "description": "This value signals that a block supports the line-height CSS style property. When it does, the block editor will show an UI control for the user to set its value if the theme declares support.\n\nWhen the block declares support for lineHeight, its attributes definition is extended to include a new attribute style of object type with no default assigned. It stores the custom value set by the user. The block can apply a default style by specifying its own style attribute with a default", "default": false } } @@ -438,7 +460,7 @@ }, "attributes": { "type": "object", - "description": "Set the attribues for the block example" + "description": "Set the attributes for the block example" }, "innerBlocks": { "type": "array", diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 53404cbdbff5e..d2fb13b6f13b4 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -20,7 +20,7 @@ "type": "object", "properties": { "appearanceTools": { - "description": "Setting that enables the following UI tools:\n\n- border: color, radius, style, width\n- color: link\n- dimensions: minHeight\n- spacing: blockGap, margin, padding\n- typography: lineHeight", + "description": "Setting that enables the following UI tools:\n\n- border: color, radius, style, width\n- color: link\n- dimensions: minHeight\n- position: sticky\n- spacing: blockGap, margin, padding\n- typography: lineHeight", "type": "boolean", "default": false } @@ -332,7 +332,7 @@ "type": "object", "properties": { "operator": { - "description": "With + or * depending on whether scale is generated by increment or mulitplier.", + "description": "With + or * depending on whether scale is generated by increment or multiplier.", "type": "string", "enum": [ "+", "*" ], "default": "*" diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index 8796de6fdb28b..e1724a61d6126 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -4,14 +4,13 @@ import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; -import { devices } from '@playwright/test'; -import type { PlaywrightTestConfig } from '@playwright/test'; +import { defineConfig, devices } from '@playwright/test'; const STORAGE_STATE_PATH = process.env.STORAGE_STATE_PATH || path.join( process.cwd(), 'artifacts/storage-states/admin.json' ); -const config: PlaywrightTestConfig = { +const config = defineConfig( { reporter: process.env.CI ? [ [ 'github' ], [ './config/flaky-tests-reporter.ts' ] ] : 'list', @@ -23,6 +22,8 @@ const config: PlaywrightTestConfig = { reportSlowTests: null, testDir: fileURLToPath( new URL( './specs', 'file:' + __filename ).href ), outputDir: path.join( process.cwd(), 'artifacts/test-results' ), + snapshotPathTemplate: + '{testDir}/{testFileDir}/__snapshots__/{arg}-{projectName}{ext}', globalSetup: fileURLToPath( new URL( './config/global-setup.ts', 'file:' + __filename ).href ), @@ -81,6 +82,6 @@ const config: PlaywrightTestConfig = { grepInvert: /-firefox/, }, ], -}; +} ); export default config; diff --git a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-changing-aspect-ratio-using-the-crop-tools-1-chromium.png b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-changing-aspect-ratio-using-the-crop-tools-1-chromium.png new file mode 100644 index 0000000000000..b488fe1a7e483 Binary files /dev/null and b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-changing-aspect-ratio-using-the-crop-tools-1-chromium.png differ diff --git a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-changing-aspect-ratio-using-the-crop-tools-1-chromium.txt b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-changing-aspect-ratio-using-the-crop-tools-1-chromium.txt deleted file mode 100644 index 023f807dff9f3..0000000000000 --- a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-changing-aspect-ratio-using-the-crop-tools-1-chromium.txt +++ /dev/null @@ -1,6 +0,0 @@ -Snapshot Diff: -- First value -+ Second value - -- data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/bAEMBAwMDBAMECAQECBALCQsQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEP/AABEIAAoACgMBIgACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAJB//EACAQAAADCQEAAAAAAAAAAAAAAAAEBQEDBgk1OHR3srX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AlMlS6eKdfnvRTgrytVTuQ86aChkqXTxTr896KcFeVqqdyHnTQH//2Q== -+ data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/bAEMBAwMDBAMECAQECBALCQsQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEP/AABEIAAYACgMBIgACEQEDEQH/xAAWAAEBAQAAAAAAAAAAAAAAAAAABwn/xAAgEAABAgUFAAAAAAAAAAAAAAAABAUBAwYJNTh0d7K1/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJTZU1T1Tx+u9FuNXnbKrdxM7RAA/9k= \ No newline at end of file diff --git a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-rotating-using-the-crop-tools-1-chromium.png b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-rotating-using-the-crop-tools-1-chromium.png new file mode 100644 index 0000000000000..470cb2fe3a459 Binary files /dev/null and b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-rotating-using-the-crop-tools-1-chromium.png differ diff --git a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-rotating-using-the-crop-tools-1-chromium.txt b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-rotating-using-the-crop-tools-1-chromium.txt deleted file mode 100644 index 4409ceaca181e..0000000000000 --- a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-rotating-using-the-crop-tools-1-chromium.txt +++ /dev/null @@ -1,6 +0,0 @@ -Snapshot Diff: -- First value -+ Second value - -- data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/bAEMBAwMDBAMECAQECBALCQsQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEP/AABEIAAoACgMBIgACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAJB//EACAQAAADCQEAAAAAAAAAAAAAAAAEBQEDBgk1OHR3srX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AlMlS6eKdfnvRTgrytVTuQ86aChkqXTxTr896KcFeVqqdyHnTQH//2Q== -+ data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/bAEMBAwMDBAMECAQECBALCQsQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEP/AABEIAAoACgMBIgACEQEDEQH/xAAWAAEBAQAAAAAAAAAAAAAAAAAABwn/xAAhEAAAAgsBAAAAAAAAAAAAAAAABQIEBwgVGVVWk5XR1P/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCUyVHp7+ZVtTHwhJUenv5lW1MfCNXosa1NbzJdCLGtTW8yXQH/2Q== \ No newline at end of file diff --git a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-zooming-using-the-crop-tools-1-chromium.png b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-zooming-using-the-crop-tools-1-chromium.png new file mode 100644 index 0000000000000..90f3057d4d52d Binary files /dev/null and b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-zooming-using-the-crop-tools-1-chromium.png differ diff --git a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-zooming-using-the-crop-tools-1-chromium.txt b/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-zooming-using-the-crop-tools-1-chromium.txt deleted file mode 100644 index 5f53c934223d3..0000000000000 --- a/test/e2e/specs/editor/blocks/__snapshots__/Image-allows-zooming-using-the-crop-tools-1-chromium.txt +++ /dev/null @@ -1,6 +0,0 @@ -Snapshot Diff: -- First value -+ Second value - -- data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/bAEMBAwMDBAMECAQECBALCQsQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEP/AABEIAAoACgMBIgACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAJB//EACAQAAADCQEAAAAAAAAAAAAAAAAEBQEDBgk1OHR3srX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AlMlS6eKdfnvRTgrytVTuQ86aChkqXTxTr896KcFeVqqdyHnTQH//2Q== -+ data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoMDwwKCw4LCQkNEQ0ODxAQERAKDBITEhATDxAQEP/bAEMBAwMDBAMECAQECBALCQsQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEP/AABEIAAUABQMBIgACEQEDEQH/xAAVAAEBAAAAAAAAAAAAAAAAAAAACP/EABoQAQABBQAAAAAAAAAAAAAAAAAEBwk4hbX/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8Aqm1xgnTLddiaAD//2Q== \ No newline at end of file diff --git a/test/e2e/specs/editor/blocks/heading.spec.js b/test/e2e/specs/editor/blocks/heading.spec.js new file mode 100644 index 0000000000000..56154f3e668e0 --- /dev/null +++ b/test/e2e/specs/editor/blocks/heading.spec.js @@ -0,0 +1,182 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Heading', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'can be created by prefixing number sign and a space', async ( { + editor, + page, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '### 3' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { content: '3', level: 3 }, + }, + ] ); + } ); + + test( 'can be created by prefixing existing content with number signs and a space', async ( { + editor, + page, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '4' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.type( '#### ' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { content: '4', level: 4 }, + }, + ] ); + } ); + + test( 'should not work with the list input rule', async ( { + editor, + page, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '## 1. H' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { content: '1. H', level: 2 }, + }, + ] ); + } ); + + test( 'should work with the format input rules', async ( { + editor, + page, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '## `code`' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { content: 'code', level: 2 }, + }, + ] ); + } ); + + test( 'should create a paragraph block above when pressing enter at the start', async ( { + editor, + page, + } ) => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '## a' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'Enter' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '' }, + }, + { + name: 'core/heading', + attributes: { content: 'a', level: 2 }, + }, + ] ); + } ); + + test( 'should create a paragraph block below when pressing enter at the end', async ( { + editor, + page, + } ) => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '## a' ); + await page.keyboard.press( 'Enter' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { content: 'a', level: 2 }, + }, + { + name: 'core/paragraph', + attributes: { content: '' }, + }, + ] ); + } ); + + test( 'should correctly apply custom colors', async ( { + editor, + page, + } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '### Heading' ); + await editor.openDocumentSettingsSidebar(); + + const textColor = page + .getByRole( 'region', { + name: 'Editor settings', + } ) + .getByRole( 'button', { name: 'Text' } ); + + await textColor.click(); + await page + .getByRole( 'button', { name: /Custom color picker./i } ) + .click(); + + await page + .getByRole( 'textbox', { name: 'Hex color' } ) + .fill( '4b7f4d' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { + content: 'Heading', + level: 3, + style: { color: { text: '#4b7f4d' } }, + }, + }, + ] ); + } ); + + test( 'should correctly apply named colors', async ( { editor, page } ) => { + await page.click( 'role=button[name="Add default block"i]' ); + await page.keyboard.type( '## Heading' ); + await editor.openDocumentSettingsSidebar(); + + const textColor = page + .getByRole( 'region', { + name: 'Editor settings', + } ) + .getByRole( 'button', { name: 'Text' } ); + + await textColor.click(); + + await page + .getByRole( 'button', { + name: 'Color: Luminous vivid orange', + } ) + .click(); + + // Close the popover. + await textColor.click(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/heading', + attributes: { + content: 'Heading', + level: 2, + textColor: 'luminous-vivid-orange', + }, + }, + ] ); + } ); +} ); diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index 26db3cb0d4254..fc435544b21bb 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -5,7 +5,8 @@ const path = require( 'path' ); const fs = require( 'fs/promises' ); const os = require( 'os' ); const { v4: uuid } = require( 'uuid' ); -const snapshotDiff = require( 'snapshot-diff' ); + +/** @typedef {import('@playwright/test').Page} Page */ /** * WordPress dependencies @@ -312,7 +313,12 @@ test.describe( 'Image', () => { // Assert that the image is initially unscaled and unedited. const initialImageSrc = await image.getAttribute( 'src' ); - const initialImageDataURL = await imageBlockUtils.getDataURL( image ); + await expect + .poll( () => image.boundingBox() ) + .toMatchObject( { + height: 10, + width: 10, + } ); // Zoom in to twice the amount using the zoom input. await editor.clickBlockToolbarButton( 'Crop' ); @@ -340,11 +346,15 @@ test.describe( 'Image', () => { const updatedImageSrc = await image.getAttribute( 'src' ); expect( initialImageSrc ).not.toEqual( updatedImageSrc ); - const updatedImageDataURL = await imageBlockUtils.getDataURL( image ); - expect( initialImageDataURL ).not.toEqual( updatedImageDataURL ); + await expect + .poll( () => image.boundingBox() ) + .toMatchObject( { + height: 5, + width: 5, + } ); expect( - snapshotDiff( initialImageDataURL, updatedImageDataURL ) + await imageBlockUtils.getImageBuffer( updatedImageSrc ) ).toMatchSnapshot(); } ); @@ -369,7 +379,12 @@ test.describe( 'Image', () => { // Assert that the image is initially unscaled and unedited. const initialImageSrc = await image.getAttribute( 'src' ); - const initialImageDataURL = await imageBlockUtils.getDataURL( image ); + await expect + .poll( () => image.boundingBox() ) + .toMatchObject( { + height: 10, + width: 10, + } ); // Zoom in to twice the amount using the zoom input. await editor.clickBlockToolbarButton( 'Crop' ); @@ -386,13 +401,17 @@ test.describe( 'Image', () => { // Assert that the image is edited. const updatedImageSrc = await image.getAttribute( 'src' ); - const updatedImageDataURL = await imageBlockUtils.getDataURL( image ); + expect( updatedImageSrc ).not.toEqual( initialImageSrc ); - expect( initialImageSrc ).not.toEqual( updatedImageSrc ); - expect( initialImageDataURL ).not.toEqual( updatedImageDataURL ); + await expect + .poll( () => image.boundingBox() ) + .toMatchObject( { + height: 6, + width: 10, + } ); expect( - snapshotDiff( initialImageDataURL, updatedImageDataURL ) + await imageBlockUtils.getImageBuffer( updatedImageSrc ) ).toMatchSnapshot(); } ); @@ -415,9 +434,6 @@ test.describe( 'Image', () => { await expect( image ).toHaveAttribute( 'src', new RegExp( filename ) ); - // Assert that the image is initially unscaled and unedited. - const initialImageDataURL = await imageBlockUtils.getDataURL( image ); - // Rotate the image. await editor.clickBlockToolbarButton( 'Crop' ); await editor.clickBlockToolbarButton( 'Rotate' ); @@ -429,14 +445,10 @@ test.describe( 'Image', () => { ).toBeHidden(); // Assert that the image is edited. - await expect - .poll( async () => imageBlockUtils.getDataURL( image ) ) - .not.toBe( initialImageDataURL ); - - const updatedImageDataURL = await imageBlockUtils.getDataURL( image ); + const updatedImageSrc = await image.getAttribute( 'src' ); expect( - snapshotDiff( initialImageDataURL, updatedImageDataURL ) + await imageBlockUtils.getImageBuffer( updatedImageSrc ) ).toMatchSnapshot(); } ); @@ -530,6 +542,7 @@ test.describe( 'Image', () => { class ImageBlockUtils { constructor( { page } ) { + /** @type {Page} */ this.page = page; this.TEST_IMAGE_FILE_PATH = path.join( @@ -555,14 +568,40 @@ class ImageBlockUtils { return filename; } - async getDataURL( element ) { + async getImageBuffer( url ) { + const response = await this.page.request.get( url ); + return await response.body(); + } + + async getHexString( element ) { return element.evaluate( ( node ) => { const canvas = document.createElement( 'canvas' ); - const context = canvas.getContext( '2d' ); canvas.width = node.width; canvas.height = node.height; + + const context = canvas.getContext( '2d' ); context.drawImage( node, 0, 0 ); - return canvas.toDataURL( 'image/jpeg' ); + const imageData = context.getImageData( + 0, + 0, + canvas.width, + canvas.height + ); + const pixels = imageData.data; + + let hexString = ''; + for ( let i = 0; i < pixels.length; i += 4 ) { + if ( i !== 0 && i % ( canvas.width * 4 ) === 0 ) { + hexString += '\n'; + } + + const r = pixels[ i ].toString( 16 ).padStart( 2, '0' ); + const g = pixels[ i + 1 ].toString( 16 ).padStart( 2, '0' ); + const b = pixels[ i + 2 ].toString( 16 ).padStart( 2, '0' ); + const a = pixels[ i + 3 ].toString( 16 ).padStart( 2, '0' ); + hexString += '#' + r + g + b + a; + } + return hexString; } ); } } diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js index 0a1159f6d5d49..505f077918a97 100644 --- a/test/e2e/specs/editor/blocks/list.spec.js +++ b/test/e2e/specs/editor/blocks/list.spec.js @@ -1247,7 +1247,7 @@ test.describe( 'List', () => { page.locator( 'role=document[name="Block: List"i]' ) ); - await page.getByRole( 'button', { name: 'List' } ).click(); + await page.getByRole( 'button', { name: 'List', exact: true } ).click(); await page.getByRole( 'menuitem', { name: 'Paragraph' } ).click(); expect( await editor.getEditedPostContent() ).toMatchSnapshot(); diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index c362e23a36b83..57ef99cbc09d6 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -4,8 +4,8 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); test.use( { - navBlockUtils: async ( { page, requestUtils }, use ) => { - await use( new NavigationBlockUtils( { page, requestUtils } ) ); + navBlockUtils: async ( { editor, page, requestUtils }, use ) => { + await use( new NavigationBlockUtils( { editor, page, requestUtils } ) ); }, } ); @@ -17,16 +17,13 @@ test.describe( await requestUtils.activateTheme( 'twentytwentythree' ); } ); - test.beforeEach( async ( { admin, navBlockUtils } ) => { - await Promise.all( [ - navBlockUtils.deleteAllNavigationMenus(), - admin.createNewPost(), - ] ); + test.beforeEach( async ( { requestUtils } ) => { + await Promise.all( [ requestUtils.deleteAllMenus() ] ); } ); - test.afterAll( async ( { requestUtils, navBlockUtils } ) => { + test.afterAll( async ( { requestUtils } ) => { await Promise.all( [ - navBlockUtils.deleteAllNavigationMenus(), + requestUtils.deleteAllMenus(), requestUtils.activateTheme( 'twentytwentyone' ), ] ); } ); @@ -36,8 +33,10 @@ test.describe( } ); test( 'default to a list of pages if there are no menus', async ( { + admin, editor, } ) => { + await admin.createNewPost(); await editor.insertBlock( { name: 'core/navigation' } ); const pageListBlock = editor.canvas.getByRole( 'document', { @@ -63,11 +62,14 @@ test.describe( } ); test( 'default to my only existing menu', async ( { + admin, editor, page, + requestUtils, navBlockUtils, } ) => { - const createdMenu = await navBlockUtils.createNavigationMenu( { + await admin.createNewPost(); + const createdMenu = await requestUtils.createNavigationMenu( { title: 'Test Menu 1', content: '', @@ -75,12 +77,8 @@ test.describe( await editor.insertBlock( { name: 'core/navigation' } ); - //check the block in the canvas. - await expect( - editor.canvas.locator( - 'role=textbox[name="Navigation link text"i] >> text="WordPress"' - ) - ).toBeVisible(); + // Check the block in the canvas. + await navBlockUtils.selectNavigationItemOnCanvas( 'WordPress' ); // Check the markup of the block is correct. await editor.publishPost(); @@ -92,13 +90,35 @@ test.describe( ] ); await page.locator( 'role=button[name="Close panel"i]' ).click(); - //check the block in the frontend. - await page.goto( '/' ); - await expect( - page.locator( - 'role=navigation >> role=link[name="WordPress"i]' - ) - ).toBeVisible(); + // Check the block in the frontend. + await navBlockUtils.selectNavigationItemOnFrontend( 'WordPress' ); + } ); + + test( 'default to the only existing classic menu if there are no block menus', async ( { + admin, + editor, + requestUtils, + navBlockUtils, + } ) => { + // Create a classic menu. + await requestUtils.createClassicMenu( 'Test Classic 1' ); + await admin.createNewPost(); + + await editor.insertBlock( { name: 'core/navigation' } ); + // We need to check the canvas after inserting the navigation block to be able to target the block. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/navigation', + }, + ] ); + + // Check the block in the canvas. + await editor.page.pause(); + await navBlockUtils.selectNavigationItemOnCanvas( 'Custom link' ); + + // Check the block in the frontend. + await navBlockUtils.selectNavigationItemOnFrontend( 'Custom link' ); + await editor.page.pause(); } ); } ); @@ -110,39 +130,61 @@ class NavigationBlockUtils { this.requestUtils = requestUtils; } - /** - * Create a navigation menu - * - * @param {Object} menuData navigation menu post data. - * @return {string} Menu content. - */ - async createNavigationMenu( menuData ) { - return this.requestUtils.rest( { - method: 'POST', - path: `/wp/v2/navigation/`, - data: { - status: 'publish', - ...menuData, - }, - } ); + async selectNavigationItemOnCanvas( name ) { + await expect( + this.editor.canvas.locator( + `role=textbox[name="Navigation link text"i] >> text="${ name }"` + ) + ).toBeVisible(); } - /** - * Delete all navigation menus - * - */ - async deleteAllNavigationMenus() { - const menus = await this.requestUtils.rest( { - path: `/wp/v2/navigation/`, - } ); - - if ( ! menus?.length ) return; - - await this.requestUtils.batchRest( - menus.map( ( menu ) => ( { - method: 'DELETE', - path: `/wp/v2/navigation/${ menu.id }?force=true`, - } ) ) - ); + async selectNavigationItemOnFrontend( name ) { + await this.page.goto( '/' ); + await this.editor.page.pause(); + await expect( + this.page.locator( + `role=navigation >> role=link[name="${ name }"i]` + ) + ).toBeVisible(); } } + +test.describe( 'Navigation block', () => { + test.describe( + 'As a user I want to see a warning if the menu referenced by a navigation block is not available', + () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'warning message shows when given an unknown ref', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/navigation', + attributes: { + ref: 1, + }, + } ); + + // Check the markup of the block is correct. + await editor.publishPost(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/navigation', + attributes: { ref: 1 }, + }, + ] ); + + // Find the warning message + const warningMessage = editor.canvas + .getByRole( 'document', { name: 'Block: Navigation' } ) + .getByText( + 'Navigation menu has been deleted or is unavailable.' + ); + await expect( warningMessage ).toBeVisible(); + } ); + } + ); +} ); diff --git a/test/e2e/specs/editor/blocks/query.spec.js b/test/e2e/specs/editor/blocks/query.spec.js new file mode 100644 index 0000000000000..79b15222630ef --- /dev/null +++ b/test/e2e/specs/editor/blocks/query.spec.js @@ -0,0 +1,64 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Query block', () => { + test.beforeAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.activatePlugin( 'gutenberg-test-query-block' ), + requestUtils.deleteAllPosts(), + requestUtils.deleteAllPages(), + ] ); + await requestUtils.createPost( { title: 'Post 1', status: 'publish' } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await Promise.all( [ + requestUtils.deleteAllPosts(), + requestUtils.deactivatePlugin( 'gutenberg-test-query-block' ), + ] ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost( { postType: 'page', title: 'Query Page' } ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPages(); + } ); + + test.describe( 'Query block insertion', () => { + test( 'List', async ( { page, editor } ) => { + await editor.insertBlock( { name: 'core/query' } ); + + await editor.canvas + .getByRole( 'document', { name: 'Block: Query Loop' } ) + .getByRole( 'button', { name: 'Choose' } ) + .click(); + + await page + .getByRole( 'dialog', { name: 'Choose a pattern' } ) + .getByRole( 'option', { name: 'Standard' } ) + .click(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/query', + innerBlocks: [ + { + name: 'core/post-template', + innerBlocks: [ + { name: 'core/post-title' }, + { name: 'core/post-featured-image' }, + { name: 'core/post-excerpt' }, + { name: 'core/separator' }, + { name: 'core/post-date' }, + ], + }, + ], + }, + ] ); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/a11y.spec.js b/test/e2e/specs/editor/various/a11y.spec.js index 6ff397fc9cab8..4e37c08b08e0c 100644 --- a/test/e2e/specs/editor/various/a11y.spec.js +++ b/test/e2e/specs/editor/various/a11y.spec.js @@ -3,7 +3,16 @@ */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); -test.describe( 'a11y', () => { +test.use( { + // Make the viewport tall enough so that some tabs panels within the + // Preferences modal are not scrollable and other tab panels are. + viewport: { + width: 1280, + height: 1024, + }, +} ); + +test.describe( 'a11y (@firefox, @webkit)', () => { test.beforeEach( async ( { admin } ) => { await admin.createNewPost(); } ); @@ -38,9 +47,13 @@ test.describe( 'a11y', () => { page, pageUtils, } ) => { - // Open keyboard help modal. + // Open keyboard shortcuts modal. await pageUtils.pressKeyWithModifier( 'access', 'h' ); + const modalContent = page.locator( + 'role=dialog[name="Keyboard shortcuts"i] >> role=document' + ); + const closeButton = page.locator( 'role=dialog[name="Keyboard shortcuts"i] >> role=button[name="Close"i]' ); @@ -49,10 +62,15 @@ test.describe( 'a11y', () => { // See: https://github.com/WordPress/gutenberg/issues/9410 await expect( closeButton ).not.toBeFocused(); + // Open keyboard shortcuts modal. await page.keyboard.press( 'Tab' ); + await expect( modalContent ).toBeFocused(); - // Ensure the Close button of the modal is focused after tabbing. + await page.keyboard.press( 'Tab' ); await expect( closeButton ).toBeFocused(); + + await page.keyboard.press( 'Tab' ); + await expect( modalContent ).toBeFocused(); } ); test( 'should return focus to the first tabbable in a modal after blurring a tabbable', async ( { @@ -93,4 +111,90 @@ test.describe( 'a11y', () => { ) ).toBeFocused(); } ); + + test( 'should make the modal content focusable when it is scrollable', async ( { + page, + } ) => { + // Open the top bar Options menu. + await page.click( + 'role=region[name="Editor top bar"i] >> role=button[name="Options"i]' + ); + + // Open the Preferences modal. + await page.click( + 'role=menu[name="Options"i] >> role=menuitem[name="Preferences"i]' + ); + + const preferencesModal = page.locator( + 'role=dialog[name="Preferences"i]' + ); + const preferencesModalContent = + preferencesModal.locator( 'role=document' ); + const closeButton = preferencesModal.locator( + 'role=button[name="Close"i]' + ); + const generalTab = preferencesModal.locator( + 'role=tab[name="General"i]' + ); + const blocksTab = preferencesModal.locator( + 'role=tab[name="Blocks"i]' + ); + const panelsTab = preferencesModal.locator( + 'role=tab[name="Panels"i]' + ); + + // Check initial focus is on the modal dialog container. + await expect( preferencesModal ).toBeFocused(); + + // Check the General tab panel is visible by default. + await expect( + preferencesModal.locator( 'role=tabpanel[name="General"i]' ) + ).toBeVisible(); + + async function clickAndFocusTab( tab ) { + // Some browsers, e.g. Safari, don't set focus after a click. We need + // to ensure focus is set to start tabbing from a predictable place + // in the UI. This isn't part of the user flow we want to test. + await tab.click(); + await tab.focus(); + } + + // The General tab panel content is short and not scrollable. + // Check it's not focusable. + await clickAndFocusTab( generalTab ); + await page.keyboard.press( 'Shift+Tab' ); + await expect( closeButton ).toBeFocused(); + await page.keyboard.press( 'Shift+Tab' ); + await expect( preferencesModalContent ).not.toBeFocused(); + + // The Blocks tab panel content is long and scrollable. + // Check it's focusable. + await clickAndFocusTab( blocksTab ); + await page.keyboard.press( 'Shift+Tab' ); + await expect( closeButton ).toBeFocused(); + await page.keyboard.press( 'Shift+Tab' ); + await expect( preferencesModalContent ).toBeFocused(); + + // Make the Blocks tab panel content shorter by searching for a block + // that doesn't exist. The content only shows 'No blocks found' and it's + // not scrollable any longer. Check it's not focusable. + await clickAndFocusTab( blocksTab ); + await page.type( + 'role=searchbox[name="Search for a block"i]', + 'qwerty' + ); + await clickAndFocusTab( blocksTab ); + await page.keyboard.press( 'Shift+Tab' ); + await expect( closeButton ).toBeFocused(); + await page.keyboard.press( 'Shift+Tab' ); + await expect( preferencesModalContent ).not.toBeFocused(); + + // The Panels tab panel content is short and not scrollable. + // Check it's not focusable. + await clickAndFocusTab( panelsTab ); + await page.keyboard.press( 'Shift+Tab' ); + await expect( closeButton ).toBeFocused(); + await page.keyboard.press( 'Shift+Tab' ); + await expect( preferencesModalContent ).not.toBeFocused(); + } ); } ); diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js index fdc4615f197aa..a0d042c06d6a0 100644 --- a/test/e2e/specs/editor/various/inserting-blocks.spec.js +++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js @@ -289,6 +289,31 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { await expect.poll( editor.getEditedPostContent ).toBe( beforeContent ); } ); + + // A test for https://github.com/WordPress/gutenberg/issues/43090. + test( 'should close the inserter when clicking on the toggle button', async ( { + page, + editor, + } ) => { + const inserterButton = page.getByRole( 'button', { + name: 'Toggle block inserter', + } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await inserterButton.click(); + + await blockLibrary.getByRole( 'option', { name: 'Buttons' } ).click(); + + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ { name: 'core/buttons' } ] ); + + await inserterButton.click(); + + await expect( blockLibrary ).toBeHidden(); + } ); } ); test.describe( 'insert media from inserter', () => { diff --git a/test/e2e/specs/site-editor/block-list-panel-preference.spec.js b/test/e2e/specs/site-editor/block-list-panel-preference.spec.js index 8745a75f3ad46..ede473816235e 100644 --- a/test/e2e/specs/site-editor/block-list-panel-preference.spec.js +++ b/test/e2e/specs/site-editor/block-list-panel-preference.spec.js @@ -12,13 +12,14 @@ test.describe( 'Block list view', () => { await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test( 'Should open by default', async ( { admin, page, siteEditor } ) => { + test( 'Should open by default', async ( { admin, page, editor } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', postType: 'wp_template', + path: '/templates/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Should display the Preview button. await expect( diff --git a/test/e2e/specs/site-editor/iframe-rendering.spec.js b/test/e2e/specs/site-editor/iframe-rendering.spec.js index 02389cc936f06..a4c39cdbbbfad 100644 --- a/test/e2e/specs/site-editor/iframe-rendering.spec.js +++ b/test/e2e/specs/site-editor/iframe-rendering.spec.js @@ -19,6 +19,7 @@ test.describe( 'Site editor iframe rendering mode', () => { await admin.visitSiteEditor( { postId: 'emptytheme//index', postType: 'wp_template', + path: '/templates/single', } ); const compatMode = await page diff --git a/test/e2e/specs/site-editor/push-to-global-styles.spec.js b/test/e2e/specs/site-editor/push-to-global-styles.spec.js index d51ce41dc3ea8..06c634a1f5498 100644 --- a/test/e2e/specs/site-editor/push-to-global-styles.spec.js +++ b/test/e2e/specs/site-editor/push-to-global-styles.spec.js @@ -16,9 +16,9 @@ test.describe( 'Push to Global Styles button', () => { await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test.beforeEach( async ( { admin, siteEditor } ) => { + test.beforeEach( async ( { admin, editor } ) => { await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); } ); test( 'should apply Heading block styles to all Heading blocks', async ( { diff --git a/test/e2e/specs/site-editor/site-editor-inserter.spec.js b/test/e2e/specs/site-editor/site-editor-inserter.spec.js index 2dc8002b03089..f8ab0a534d858 100644 --- a/test/e2e/specs/site-editor/site-editor-inserter.spec.js +++ b/test/e2e/specs/site-editor/site-editor-inserter.spec.js @@ -16,9 +16,9 @@ test.describe( 'Site Editor Inserter', () => { await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test.beforeEach( async ( { admin, siteEditor } ) => { + test.beforeEach( async ( { admin, editor } ) => { await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); } ); test( 'inserter toggle button should toggle global inserter', async ( { @@ -40,4 +40,31 @@ test.describe( 'Site Editor Inserter', () => { ) ).toBeHidden(); } ); + + // A test for https://github.com/WordPress/gutenberg/issues/43090. + test( 'should close the inserter when clicking on the toggle button', async ( { + page, + editor, + } ) => { + const inserterButton = page.getByRole( 'button', { + name: 'Toggle block inserter', + } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + const beforeBlocks = await editor.getBlocks(); + + await inserterButton.click(); + await blockLibrary.getByRole( 'tab', { name: 'Blocks' } ).click(); + await blockLibrary.getByRole( 'option', { name: 'Buttons' } ).click(); + + await expect + .poll( editor.getBlocks ) + .toMatchObject( [ ...beforeBlocks, { name: 'core/buttons' } ] ); + + await inserterButton.click(); + + await expect( blockLibrary ).toBeHidden(); + } ); } ); diff --git a/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js index 4f3d092a5c8d9..cf7fa8dfa67c9 100644 --- a/test/e2e/specs/site-editor/style-book.spec.js +++ b/test/e2e/specs/site-editor/style-book.spec.js @@ -18,9 +18,9 @@ test.describe( 'Style Book', () => { await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test.beforeEach( async ( { admin, siteEditor, styleBook, page } ) => { + test.beforeEach( async ( { admin, editor, styleBook, page } ) => { await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); await styleBook.open(); await expect( page.locator( 'role=region[name="Style Book"i]' ) @@ -98,7 +98,7 @@ test.describe( 'Style Book', () => { ).toBeVisible(); } ); - test( 'should clear Global Styles navigator history when example is clicked', async ( { + test( 'should allow to return Global Styles root when example is clicked', async ( { page, } ) => { await page.click( 'role=button[name="Blocks styles"]' ); @@ -109,11 +109,12 @@ test.describe( 'Style Book', () => { 'role=button[name="Open Quote styles in Styles panel"i]' ); + await page.click( 'role=button[name="Navigate to the previous view"]' ); await page.click( 'role=button[name="Navigate to the previous view"]' ); await expect( - page.locator( 'role=button[name="Navigate to the previous view"]' ) - ).not.toBeVisible(); + page.locator( 'role=button[name="Blocks styles"]' ) + ).toBeVisible(); } ); test( 'should disappear when closed', async ( { page } ) => { diff --git a/test/e2e/specs/site-editor/style-variations.spec.js b/test/e2e/specs/site-editor/style-variations.spec.js index 925be3780c8fd..d662238535745 100644 --- a/test/e2e/specs/site-editor/style-variations.spec.js +++ b/test/e2e/specs/site-editor/style-variations.spec.js @@ -33,13 +33,14 @@ test.describe( 'Global styles variations', () => { admin, page, siteEditorStyleVariations, - siteEditor, + editor, } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', + path: '/templates/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); await siteEditorStyleVariations.browseStyles(); @@ -70,13 +71,14 @@ test.describe( 'Global styles variations', () => { admin, page, siteEditorStyleVariations, - siteEditor, + editor, } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', + path: '/templates/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); await siteEditorStyleVariations.browseStyles(); await page.click( 'role=button[name="pink"i]' ); await page.click( @@ -111,13 +113,14 @@ test.describe( 'Global styles variations', () => { admin, page, siteEditorStyleVariations, - siteEditor, + editor, } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', + path: '/templates/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); await siteEditorStyleVariations.browseStyles(); await page.click( 'role=button[name="yellow"i]' ); await page.click( @@ -158,13 +161,14 @@ test.describe( 'Global styles variations', () => { admin, page, siteEditorStyleVariations, - siteEditor, + editor, } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', + path: '/templates/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); await siteEditorStyleVariations.browseStyles(); await page.click( 'role=button[name="pink"i]' ); await page.click( @@ -190,13 +194,14 @@ test.describe( 'Global styles variations', () => { admin, page, siteEditorStyleVariations, - siteEditor, + editor, } ) => { await admin.visitSiteEditor( { postId: 'gutenberg-test-themes/style-variations//index', postType: 'wp_template', + path: '/templates/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); await siteEditorStyleVariations.browseStyles(); await page.click( 'role=button[name="yellow"i]' ); diff --git a/test/e2e/specs/site-editor/template-part.spec.js b/test/e2e/specs/site-editor/template-part.spec.js index 8002fafad107c..c33da19290855 100644 --- a/test/e2e/specs/site-editor/template-part.spec.js +++ b/test/e2e/specs/site-editor/template-part.spec.js @@ -23,13 +23,13 @@ test.describe( 'Template Part', () => { admin, editor, page, - siteEditor, } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//header', postType: 'wp_template_part', + path: '/template-parts/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Insert a new template block and 'start blank'. await editor.insertBlock( { name: 'core/template-part' } ); @@ -54,11 +54,10 @@ test.describe( 'Template Part', () => { admin, editor, page, - siteEditor, } ) => { // Visit the index. await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); const headerTemplateParts = editor.canvas.locator( '[data-type="core/template-part"]' ); @@ -81,12 +80,11 @@ test.describe( 'Template Part', () => { admin, editor, page, - siteEditor, } ) => { const paragraphText = 'Test 2'; await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Add a block and select it. await editor.insertBlock( { name: 'core/paragraph', @@ -121,13 +119,12 @@ test.describe( 'Template Part', () => { admin, editor, page, - siteEditor, } ) => { const paragraphText1 = 'Test 3'; const paragraphText2 = 'Test 4'; await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Add a block and select it. await editor.insertBlock( { name: 'core/paragraph', @@ -181,7 +178,6 @@ test.describe( 'Template Part', () => { test( 'can detach blocks from a template part', async ( { admin, editor, - siteEditor, } ) => { const paragraphText = 'Test 3'; @@ -189,8 +185,9 @@ test.describe( 'Template Part', () => { await admin.visitSiteEditor( { postId: 'emptytheme//header', postType: 'wp_template_part', + path: '/template-parts/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); await editor.insertBlock( { name: 'core/paragraph', attributes: { @@ -201,7 +198,7 @@ test.describe( 'Template Part', () => { // Visit the index. await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Check that the header contains the paragraph added earlier. const paragraph = editor.canvas.locator( `p >> text="${ paragraphText }"` @@ -226,15 +223,15 @@ test.describe( 'Template Part', () => { test( 'shows changes in a template when a template part it contains is modified', async ( { admin, editor, - siteEditor, } ) => { const paragraphText = 'Test 1'; await admin.visitSiteEditor( { postId: 'emptytheme//header', postType: 'wp_template_part', + path: '/template-parts/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Edit the header. await editor.insertBlock( { name: 'core/paragraph', @@ -247,7 +244,7 @@ test.describe( 'Template Part', () => { // Visit the index. await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); const paragraph = editor.canvas.locator( `p >> text="${ paragraphText }"` ); @@ -260,15 +257,15 @@ test.describe( 'Template Part', () => { admin, editor, page, - siteEditor, } ) => { const paragraphText = 'Test 4'; await admin.visitSiteEditor( { postId: 'emptytheme//header', postType: 'wp_template_part', + path: '/template-parts/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); await editor.insertBlock( { name: 'core/paragraph', attributes: { @@ -302,12 +299,11 @@ test.describe( 'Template Part', () => { test( 'can import a widget area into an empty template part', async ( { admin, - siteEditor, editor, page, } ) => { await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Add a block and select it. await editor.insertBlock( { @@ -344,12 +340,11 @@ test.describe( 'Template Part', () => { test( 'can not import a widget area into a non-empty template part', async ( { admin, - siteEditor, editor, page, } ) => { await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Select existing header template part. await editor.selectBlocks( diff --git a/test/e2e/specs/site-editor/template-revert.spec.js b/test/e2e/specs/site-editor/template-revert.spec.js index dcf8ebedb8b78..f1f6b3eb5d014 100644 --- a/test/e2e/specs/site-editor/template-revert.spec.js +++ b/test/e2e/specs/site-editor/template-revert.spec.js @@ -20,10 +20,10 @@ test.describe( 'Template Revert', () => { await requestUtils.deleteAllTemplates( 'wp_template_part' ); await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test.beforeEach( async ( { admin, requestUtils, siteEditor } ) => { + test.beforeEach( async ( { admin, requestUtils, editor } ) => { await requestUtils.deleteAllTemplates( 'wp_template' ); await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); } ); test( 'should delete the template after saving the reverted template', async ( { @@ -248,7 +248,6 @@ test.describe( 'Template Revert', () => { editor, page, templateRevertUtils, - siteEditor, } ) => { await editor.insertBlock( { name: 'core/paragraph', @@ -267,7 +266,7 @@ test.describe( 'Template Revert', () => { await editor.saveSiteEditorEntities(); await admin.visitSiteEditor(); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); const contentAfter = await templateRevertUtils.getCurrentSiteEditorContent(); expect( contentAfter ).toEqual( contentBefore ); diff --git a/test/e2e/specs/site-editor/title.spec.js b/test/e2e/specs/site-editor/title.spec.js index 9b1ad4a5098d1..d8059531aabc5 100644 --- a/test/e2e/specs/site-editor/title.spec.js +++ b/test/e2e/specs/site-editor/title.spec.js @@ -15,14 +15,15 @@ test.describe( 'Site editor title', () => { test( 'displays the selected template name in the title for the index template', async ( { admin, page, - siteEditor, + editor, } ) => { // Navigate to a template. await admin.visitSiteEditor( { postId: 'emptytheme//index', postType: 'wp_template', + path: '/templates/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); const title = await page.locator( 'role=region[name="Editor top bar"i] >> role=heading[level=1]' ); @@ -33,14 +34,15 @@ test.describe( 'Site editor title', () => { test( 'displays the selected template name in the title for the header template', async ( { admin, page, - siteEditor, + editor, } ) => { // Navigate to a template part. await admin.visitSiteEditor( { postId: 'emptytheme//header', postType: 'wp_template_part', + path: '/template-parts/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); const title = await page.locator( 'role=region[name="Editor top bar"i] >> role=heading[level=1]' ); @@ -51,13 +53,14 @@ test.describe( 'Site editor title', () => { test( "displays the selected template part's name in the secondary title when a template part is selected from List View", async ( { admin, page, - siteEditor, + editor, } ) => { await admin.visitSiteEditor( { postId: 'emptytheme//index', postType: 'wp_template', + path: '/templates/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Select the header template part via list view. await page.click( 'role=button[name="List View"i]' ); const listView = await page.locator( diff --git a/test/e2e/specs/site-editor/writing-flow.spec.js b/test/e2e/specs/site-editor/writing-flow.spec.js index f8fa78b4fd812..0f8d4773b84c5 100644 --- a/test/e2e/specs/site-editor/writing-flow.spec.js +++ b/test/e2e/specs/site-editor/writing-flow.spec.js @@ -18,14 +18,14 @@ test.describe( 'Site editor writing flow', () => { editor, page, pageUtils, - siteEditor, } ) => { // Navigate to a template part with only a couple of blocks. await admin.visitSiteEditor( { postId: 'emptytheme//header', postType: 'wp_template_part', + path: '/template-parts/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Select the first site title block. const siteTitleBlock = editor.canvas.locator( 'role=document[name="Block: Site Title"i]' @@ -47,14 +47,14 @@ test.describe( 'Site editor writing flow', () => { editor, page, pageUtils, - siteEditor, } ) => { // Navigate to a template part with only a couple of blocks. await admin.visitSiteEditor( { postId: 'emptytheme//header', postType: 'wp_template_part', + path: '/template-parts/single', } ); - await siteEditor.enterEditMode(); + await editor.canvas.click( 'body' ); // Make sure the sidebar is open. await editor.openDocumentSettingsSidebar(); diff --git a/test/storybook-playwright/specs/font-size-picker.spec.ts b/test/storybook-playwright/specs/font-size-picker.spec.ts index 6aa11e89262c9..c2f6fc3580681 100644 --- a/test/storybook-playwright/specs/font-size-picker.spec.ts +++ b/test/storybook-playwright/specs/font-size-picker.spec.ts @@ -1,7 +1,7 @@ /** - * WordPress dependencies + * External dependencies */ -import { test, expect } from '@wordpress/e2e-test-utils-playwright'; +import { test, expect } from '@playwright/test'; /** * Internal dependencies diff --git a/test/storybook-playwright/specs/popover.spec.ts b/test/storybook-playwright/specs/popover.spec.ts index f11a20538edfc..247e61ffc8138 100644 --- a/test/storybook-playwright/specs/popover.spec.ts +++ b/test/storybook-playwright/specs/popover.spec.ts @@ -1,7 +1,7 @@ /** - * WordPress dependencies + * External dependencies */ -import { test, expect } from '@wordpress/e2e-test-utils-playwright'; +import { test, expect } from '@playwright/test'; /** * Internal dependencies