diff --git a/backport-changelog/6.6/6590.md b/backport-changelog/6.6/6590.md
new file mode 100644
index 0000000000000..47ef89e0db40c
--- /dev/null
+++ b/backport-changelog/6.6/6590.md
@@ -0,0 +1,5 @@
+https://github.com/WordPress/wordpress-develop/pull/6590
+
+* https://github.com/WordPress/gutenberg/pull/59531
+* https://github.com/WordPress/gutenberg/pull/61182
+* https://github.com/WordPress/gutenberg/pull/61717
diff --git a/backport-changelog/6.6/6616.md b/backport-changelog/6.6/6616.md
new file mode 100644
index 0000000000000..91261f78fb5c7
--- /dev/null
+++ b/backport-changelog/6.6/6616.md
@@ -0,0 +1,5 @@
+https://github.com/WordPress/wordpress-develop/pull/6616
+
+* https://github.com/WordPress/gutenberg/pull/58409
+* https://github.com/WordPress/gutenberg/pull/61328
+* https://github.com/WordPress/gutenberg/pull/61842
\ No newline at end of file
diff --git a/backport-changelog/6.6/6662.md b/backport-changelog/6.6/6662.md
index 2dfbc68dd23e0..5b25fc9930491 100644
--- a/backport-changelog/6.6/6662.md
+++ b/backport-changelog/6.6/6662.md
@@ -1,3 +1,4 @@
https://github.com/WordPress/wordpress-develop/pull/6662
* https://github.com/WordPress/gutenberg/pull/57908
+* https://github.com/WordPress/gutenberg/pull/62125
diff --git a/backport-changelog/6.6/6694.md b/backport-changelog/6.6/6694.md
new file mode 100644
index 0000000000000..a9eb5a7f37ef5
--- /dev/null
+++ b/backport-changelog/6.6/6694.md
@@ -0,0 +1,3 @@
+https://github.com/WordPress/wordpress-develop/pull/6694
+
+* https://github.com/WordPress/gutenberg/pull/60694
diff --git a/docs/how-to-guides/themes/global-settings-and-styles.md b/docs/how-to-guides/themes/global-settings-and-styles.md
index 69f0606c93649..a5c3e828a2d66 100644
--- a/docs/how-to-guides/themes/global-settings-and-styles.md
+++ b/docs/how-to-guides/themes/global-settings-and-styles.md
@@ -336,16 +336,7 @@ The following presets can be defined via `theme.json`:
- `color.palette`:
- generates 3 classes per preset value: color, background-color, and border-color.
- generates a single custom property per preset value.
-- `spacing.spacingScale`: used to generate an array of spacing preset sizes for use with padding, margin, and gap settings.
- - `operator`: specifies how to calculate the steps with either `*` for multiplier, or `+` for sum.
- - `increment`: the amount to increment each step by. Core by default uses a 'perfect 5th' multiplier of `1.5`.
- - `steps`: the number of steps to generate in the spacing scale. The default is 7. To prevent the generation of the spacing presets, and to disable the related UI, this can be set to `0`.
- - `mediumStep`: the steps in the scale are generated descending and ascending from a medium step, so this should be the size value of the medium space, without the unit. The default medium step is `1.5rem` so the mediumStep value is `1.5`.
- - `unit`: the unit the scale uses, eg. `px, rem, em, %`. The default is `rem`.
-- `spacing.spacingSizes`: themes can choose to include a static `spacing.spacingSizes` array of spacing preset sizes if they have a sequence of sizes that can't be generated via an increment or multiplier.
- - `name`: a human readable name for the size, eg. `Small, Medium, Large`.
- - `slug`: the machine readable name. In order to provide the best cross site/theme compatibility the slugs should be in the format, "10","20","30","40","50","60", with "50" representing the `Medium` size value.
- - `size`: the size, including the unit, eg. `1.5rem`. It is possible to include fluid values like `clamp(2rem, 10vw, 20rem)`.
+- `spacing.spacingSizes`/`spacing.spacingScale`: generates a single custom property per preset value.
- `typography.fontSizes`: generates a single class and custom property per preset value.
- `typography.fontFamilies`: generates a single custom property per preset value.
diff --git a/docs/reference-guides/block-api/block-variations.md b/docs/reference-guides/block-api/block-variations.md
index 0790b7f02641e..ffd3cc49adda8 100644
--- a/docs/reference-guides/block-api/block-variations.md
+++ b/docs/reference-guides/block-api/block-variations.md
@@ -129,9 +129,9 @@ While the `isActive` property is optional, it's recommended. This API is used by
If `isActive` is not set, the Editor cannot distinguish between an instance of the original block and your variation, so the original block information will be displayed.
-The property can be set to either a function or an array of strings (`string[]`).
+The property can be set to either an array of strings (`string[]`), or a function. It is recommended to use the string array version whenever possible.
-The function version of this property accepts a block instance's `blockAttributes` as the first argument, and the `variationAttributes` declared for a variation as the second argument. These arguments can be used to determine if a variation is active by comparing them and returning a `true` or `false` (indicating whether this variation is inactive for this block instance).
+The `string[]` version is used to declare which of the block instance's attributes should be compared to the given variation's. Each attribute will be checked and the variation will be active if all of them match.
As an example, in the core Embed block, the `providerNameSlug` attribute is used to determine the embed provider (e.g. 'youtube' or 'twitter'). The variations may be declared like this:
@@ -162,28 +162,32 @@ const variations = [
]
```
- The `isActive` function can compare the block instance value for `providerNameSlug` to the value declared in the variation's declaration (the values in the code snippet above) to determine which embed variation is active:
+The `isActive` property would then look like this:
```js
-isActive: ( blockAttributes, variationAttributes ) =>
- blockAttributes.providerNameSlug === variationAttributes.providerNameSlug,
+isActive: [ 'providerNameSlug' ]
```
-The `string[]` version is used to declare which attributes should be compared as a shorthand. Each attribute will be checked and the variation will be active if all of them match. Using the same example for the embed block, the string version would look like this:
+This will cause the block instance value for `providerNameSlug` to be compared to the value declared in the variation's declaration (the values in the code snippet above) to determine which embed variation is active.
+
+Nested object paths are also supported. For example, consider a block variation that has a `query` object as an attribute. It is possible to determine if the variation is active solely based on that object's `postType` property (while ignoring all its other properties):
```js
-isActive: [ 'providerNameSlug' ]
+isActive: [ 'query.postType' ]
```
-Nested object paths are also supported. For example, consider a block variation that has a `query` object as an attribute. It is possible to determine if the variation is active solely based on that object's `postType` property (while ignoring all its other properties):
+The function version of this property accepts a block instance's `blockAttributes` as the first argument, and the `variationAttributes` declared for a variation as the second argument. These arguments can be used to determine if a variation is active by comparing them and returning a `true` or `false` (indicating whether this variation is inactive for this block instance).
+
+Using the same example for the embed block, the function version would look like this:
```js
-isActive: [ 'query.postType' ]
+isActive: ( blockAttributes, variationAttributes ) =>
+ blockAttributes.providerNameSlug === variationAttributes.providerNameSlug,
```
-### Caveats to using `isActive`
+### Specificity of `isActive` matches
-The `isActive` property can return false positives if multiple variations exist for a specific block and the `isActive` checks are not specific enough. To demonstrate this, consider the following example:
+If there are multiple variations whose `isActive` check matches a given block instance, and all of them are string arrays, then the variation with the highest _specificity_ will be chosen. Consider the following example:
```js
wp.blocks.registerBlockVariation(
@@ -212,6 +216,6 @@ wp.blocks.registerBlockVariation(
);
```
-The `isActive` check on both variations tests the `textColor`, but each variations uses `vivid-red`. Since the `paragraph-red` variation is registered first, once the `paragraph-red-grey` variation is inserted into the Editor, it will have the title `Red Paragraph` instead of `Red/Grey Paragraph`. As soon as the Editor finds a match, it stops checking.
+If a block instance has attributes `textColor: vivid-red` and `backgroundColor: cyan-bluish-gray`, both variations' `isActive` criterion will match that block instance. In this case, the more _specific_ match will be determined to be the active variation, where specificity is calculated as the length of each `isActive` array. This means that the `Red/Grey Paragraph` will be shown as the active variation.
-There have been [discussions](https://github.com/WordPress/gutenberg/issues/41303#issuecomment-1526193087) around how the API can be improved, but as of WordPress 6.3, this remains an issue to watch out for.
+Note that specificity cannot be determined for a matching variation if its `isActive` property is a function rather than a `string[]`. In this case, the first matching variation will be determined to be the active variation. For this reason, it is generally recommended to use a `string[]` rather than a `function` for the `isActive` property.
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 f752fe8104a56..59a820a16697c 100644
--- a/docs/reference-guides/theme-json-reference/theme-json-living.md
+++ b/docs/reference-guides/theme-json-reference/theme-json-living.md
@@ -167,6 +167,7 @@ Settings related to spacing.
| padding | boolean | false | |
| units | array | px,em,rem,vh,vw,% | |
| customSpacingSize | boolean | true | |
+| defaultSpacingSizes | boolean | true | |
| spacingSizes | array | | name, size, slug |
| spacingScale | object | | |
diff --git a/docs/reference-guides/theme-json-reference/theme-json-migrations.md b/docs/reference-guides/theme-json-reference/theme-json-migrations.md
index c304bfe39493e..8e9d56ed054e6 100644
--- a/docs/reference-guides/theme-json-reference/theme-json-migrations.md
+++ b/docs/reference-guides/theme-json-reference/theme-json-migrations.md
@@ -88,8 +88,27 @@ The new `defaultFontSizes` option gives control over showing default font sizes
It is `true` by default when switching to v3. This is to be consistent with how other `default*` options work such as `settings.color.defaultPalette`, but differs from the behavior in v2.
-In theme.json v2, the default font sizes were only shown when theme sizes were not defined. A theme providing font sizes with the same slugs as the defaults would always override the default ones.
-
To keep behavior similar to v2 with a v3 theme.json:
* If you do not have any `fontSizes` defined, `defaultFontSizes` can be left out or set to `true`.
* If you have some `fontSizes` defined, set `defaultFontSizes` to `false`.
+
+#### `settings.spacing.defaultSpacingSizes`
+
+In theme.json v2, there are two settings that could be used to set theme level spacing sizes: `settings.spacing.spacingSizes` and `settings.spacing.spacingScale`. Setting both `spacingSizes` _and_ `spacingScale` would only use the values from `spacingSizes`. And setting either of them would always replace the entire set of default spacing sizes provided by WordPress.
+
+The default `spacingSizes` slugs provided by WordPress are: `20`, `30`, `40`, `50`, `60`, `70`, and `80`.
+
+The new `defaultSpacingSizes` option gives control over showing default spacing sizes and preventing those defaults from being overridden.
+
+- When set to `true` it will show the default spacing sizes and prevent them from being overridden by the theme.
+- When set to `false` it will hide the default spacing sizes and allow the theme to use the default slugs.
+
+`defaultSpacingSizes` is `true` by default when switching to v3. This is to be consistent with how other `default*` options work such as `settings.color.defaultPalette`, but differs from the behavior in v2.
+
+Additionally, in v3 both `spacingSizes` and `spacingScale` can be set at the same time. Presets defined in `spacingSizes` with slugs matching the generated presets from `spacingSizes` will override the generated ones.
+
+To keep behavior similar to v2 with a v3 theme.json:
+* If you do not have any `spacingSizes` presets or `spacingScale` config defined, `defaultSpacingSizes` can be left out or set to `true`.
+* If you disabled default spacing sizes by setting `spacingScale` to `{ "steps": 0 }`, remove the `spacingScale` config and set `defaultSpacingSizes` to `false`.
+* If you defined only one of either `spacingScale` or `spacingSizes` for your presets, set `defaultSpacingSizes` to `false`.
+* If you defined both `spacingScale` and `spacingSizes`, remove the `spacingSizes` config _and_ set `defaultSpacingSizes` to `false`.
diff --git a/lib/block-supports/block-style-variations.php b/lib/block-supports/block-style-variations.php
index 6fc89b01c6793..e09bd4ce90bf0 100644
--- a/lib/block-supports/block-style-variations.php
+++ b/lib/block-supports/block-style-variations.php
@@ -7,17 +7,17 @@
*/
/**
- * Get the class name for this application of this block's variation styles.
+ * Generate block style variation instance name.
*
* @since 6.6.0
*
* @param array $block Block object.
* @param string $variation Slug for the block style variation.
*
- * @return string The unique class name.
+ * @return string The unique variation name.
*/
-function gutenberg_get_block_style_variation_class_name( $block, $variation ) {
- return 'is-style-' . $variation . '--' . md5( serialize( $block ) );
+function gutenberg_create_block_style_variation_instance_name( $block, $variation ) {
+ return $variation . '--' . md5( serialize( $block ) );
}
/**
@@ -79,31 +79,69 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block )
return $parsed_block;
}
+ $variation_instance = gutenberg_create_block_style_variation_instance_name( $parsed_block, $variation );
+ $class_name = "is-style-$variation_instance";
+ $updated_class_name = $parsed_block['attrs']['className'] . " $class_name";
+
+ /*
+ * Even though block style variations are effectively theme.json partials,
+ * they can't be processed completely as though they are.
+ *
+ * Block styles support custom selectors to direct specific types of styles
+ * to inner elements. For example, borders on Image block's get applied to
+ * the inner `img` element rather than the wrapping `figure`.
+ *
+ * The following relocates the "root" block style variation styles to
+ * under an appropriate blocks property to leverage the preexisting style
+ * generation for simple block style variations. This way they get the
+ * custom selectors they need.
+ *
+ * The inner elements and block styles for the variation itself are
+ * still included at the top level but scoped by the variation's selector
+ * when the stylesheet is generated.
+ */
+ $elements_data = $variation_data['elements'] ?? array();
+ $blocks_data = $variation_data['blocks'] ?? array();
+ unset( $variation_data['elements'] );
+ unset( $variation_data['blocks'] );
+
+ _wp_array_set(
+ $blocks_data,
+ array( $parsed_block['blockName'], 'variations', $variation_instance ),
+ $variation_data
+ );
+
$config = array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
- 'styles' => $variation_data,
+ 'styles' => array(
+ 'elements' => $elements_data,
+ 'blocks' => $blocks_data,
+ ),
);
- $class_name = gutenberg_get_block_style_variation_class_name( $parsed_block, $variation );
- $updated_class_name = $parsed_block['attrs']['className'] . " $class_name";
-
- $class_name = ".$class_name";
-
+ // Turn off filter that excludes block nodes. They are needed here for the variation's inner block types.
if ( ! is_admin() ) {
remove_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' );
}
+ // Temporarily prevent variation instance from being sanitized while processing theme.json.
+ $styles_registry = WP_Block_Styles_Registry::get_instance();
+ $styles_registry->register( $parsed_block['blockName'], array( 'name' => $variation_instance ) );
+
$variation_theme_json = new WP_Theme_JSON_Gutenberg( $config, 'blocks' );
$variation_styles = $variation_theme_json->get_stylesheet(
array( 'styles' ),
array( 'custom' ),
array(
- 'root_selector' => $class_name,
'skip_root_layout_styles' => true,
- 'scope' => $class_name,
+ 'scope' => ".$class_name",
)
);
+ // Clean up temporary block style now instance styles have been processed.
+ $styles_registry->unregister( $parsed_block['blockName'], $variation_instance );
+
+ // Restore filter that excludes block nodes.
if ( ! is_admin() ) {
add_filter( 'wp_theme_json_get_style_nodes', 'wp_filter_out_block_nodes' );
}
@@ -112,7 +150,7 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block )
return $parsed_block;
}
- wp_register_style( 'block-style-variation-styles', false, array( 'global-styles' ) );
+ wp_register_style( 'block-style-variation-styles', false, array( 'global-styles', 'wp-block-library' ) );
wp_add_inline_style( 'block-style-variation-styles', $variation_styles );
/*
@@ -147,7 +185,7 @@ function gutenberg_render_block_style_variation_class_name( $block_content, $blo
* Matches a class prefixed by `is-style`, followed by the
* variation slug, then `--`, and finally a hash.
*
- * See `gutenberg_get_block_style_variation_class_name` for class generation.
+ * See `gutenberg_create_block_style_variation_instance_name` for class generation.
*/
preg_match( '/\bis-style-(\S+?--\w+)\b/', $block['attrs']['className'], $matches );
@@ -179,7 +217,7 @@ function gutenberg_render_block_style_variation_class_name( $block_content, $blo
*
* @param array $variations Shared block style variations.
*
- * @return array Block variations data to be merged under styles.blocks
+ * @return array Block variations data to be merged under `styles.blocks`.
*/
function gutenberg_resolve_and_register_block_style_variations( $variations ) {
$variations_data = array();
diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php
index 938d8afa9d4fa..ad4e2fe105b0c 100644
--- a/lib/class-wp-theme-json-gutenberg.php
+++ b/lib/class-wp-theme-json-gutenberg.php
@@ -123,6 +123,7 @@ class WP_Theme_JSON_Gutenberg {
* @since 6.0.0 Replaced `override` with `prevent_override` and updated the
* `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
* @since 6.2.0 Added 'shadow' presets.
+ * @since 6.6.0 Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes' and spacing size presets to use `spacing.defaultSpacingSizes`.
* @since 6.6.0 Added `aspectRatios`.
* @var array
*/
@@ -187,7 +188,7 @@ class WP_Theme_JSON_Gutenberg {
),
array(
'path' => array( 'spacing', 'spacingSizes' ),
- 'prevent_override' => false,
+ 'prevent_override' => array( 'spacing', 'defaultSpacingSizes' ),
'use_default_names' => true,
'value_key' => 'size',
'css_vars' => '--wp--preset--spacing--$slug',
@@ -427,13 +428,14 @@ class WP_Theme_JSON_Gutenberg {
'sticky' => null,
),
'spacing' => array(
- 'customSpacingSize' => null,
- 'spacingSizes' => null,
- 'spacingScale' => null,
- 'blockGap' => null,
- 'margin' => null,
- 'padding' => null,
- 'units' => null,
+ 'customSpacingSize' => null,
+ 'defaultSpacingSizes' => null,
+ 'spacingSizes' => null,
+ 'spacingScale' => null,
+ 'blockGap' => null,
+ 'margin' => null,
+ 'padding' => null,
+ 'units' => null,
),
'shadow' => array(
'presets' => null,
@@ -727,6 +729,8 @@ public static function get_element_class_name( $element ) {
* Constructor.
*
* @since 5.8.0
+ * @since 6.6.0 Key spacingScale by origin, and pre-generate the
+ * spacingSizes from spacingScale.
*
* @param array $theme_json A structure that follows the theme.json schema.
* @param string $origin Optional. What source of data this object represents.
@@ -742,8 +746,8 @@ public function __construct( $theme_json = array( 'version' => WP_Theme_JSON_Gut
$valid_block_names = array_keys( $registry->get_all_registered() );
$valid_element_names = array_keys( static::ELEMENTS );
$valid_variations = static::get_valid_block_style_variations();
- $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
- $this->theme_json = static::maybe_opt_in_into_settings( $theme_json );
+ $this->theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
+ $this->theme_json = static::maybe_opt_in_into_settings( $this->theme_json );
// Internally, presets are keyed by origin.
$nodes = static::get_setting_nodes( $this->theme_json );
@@ -762,6 +766,27 @@ public function __construct( $theme_json = array( 'version' => WP_Theme_JSON_Gut
}
}
}
+
+ // In addition to presets, spacingScale (which generates presets) is also keyed by origin.
+ $scale_path = array( 'settings', 'spacing', 'spacingScale' );
+ $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
+ if ( null !== $spacing_scale ) {
+ // If the spacingScale is not already keyed by origin.
+ if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) {
+ _wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) );
+ }
+ }
+
+ // Pre-generate the spacingSizes from spacingScale.
+ $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
+ $spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
+ if ( isset( $spacing_scale ) ) {
+ $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
+ $spacing_sizes = _wp_array_get( $this->theme_json, $sizes_path, array() );
+ $spacing_scale_sizes = static::compute_spacing_sizes( $spacing_scale );
+ $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
+ _wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes );
+ }
}
/**
@@ -2947,6 +2972,8 @@ protected static function get_metadata_boolean( $data, $path, $default_value = f
*
* @since 5.8.0
* @since 5.9.0 Duotone preset also has origins.
+ * @since 6.6.0 Use the spacingScale keyed by origin, and re-generate the
+ * spacingSizes from spacingScale.
*
* @param WP_Theme_JSON_Gutenberg $incoming Data to merge.
*/
@@ -2954,6 +2981,40 @@ public function merge( $incoming ) {
$incoming_data = $incoming->get_raw_data();
$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );
+ /*
+ * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor
+ * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so
+ * we can allow partial spacingScale data to inherit missing data from earlier layers when
+ * computing the spacing sizes.
+ *
+ * This happens before the presets are merged to ensure that default spacing sizes can be
+ * removed from the theme origin if $prevent_override is true.
+ */
+ $flattened_spacing_scale = array();
+ foreach ( static::VALID_ORIGINS as $origin ) {
+ $scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );
+
+ // Apply the base spacing scale to the current layer.
+ $base_spacing_scale = _wp_array_get( $this->theme_json, $scale_path, array() );
+ $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale );
+
+ $spacing_scale = _wp_array_get( $incoming_data, $scale_path, null );
+ if ( ! isset( $spacing_scale ) ) {
+ continue;
+ }
+
+ // Allow partial scale settings by merging with lower layers.
+ $flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale );
+
+ // Generate and merge the scales for this layer.
+ $sizes_path = array( 'settings', 'spacing', 'spacingSizes', $origin );
+ $spacing_sizes = _wp_array_get( $incoming_data, $sizes_path, array() );
+ $spacing_scale_sizes = static::compute_spacing_sizes( $flattened_spacing_scale );
+ $merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
+
+ _wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes );
+ }
+
/*
* The array_replace_recursive algorithm merges at the leaf level,
* but we don't want leaf arrays to be merged, so we overwrite it.
@@ -3733,12 +3794,19 @@ public function get_data() {
/**
* Sets the spacingSizes array based on the spacingScale values from theme.json.
*
+ * No longer used since theme.json version 3 as the spacingSizes are now
+ * automatically generated during construction and merge instead of manually
+ * set in the resolver.
+ *
* @since 6.1.0
+ * @deprecated 6.6.0
*
* @return null|void
*/
public function set_spacing_sizes() {
- $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale'] ?? array();
+ _deprecated_function( __METHOD__, '6.6.0' );
+
+ $spacing_scale = $this->theme_json['settings']['spacing']['spacingScale']['default'] ?? array();
// Gutenberg didn't have the 1st isset check.
if ( ! isset( $spacing_scale['steps'] )
@@ -3762,6 +3830,94 @@ public function set_spacing_sizes() {
return null;
}
+ $spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
+
+ // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes.
+ if ( $spacing_scale['steps'] <= 7 ) {
+ for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
+ $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
+ }
+ }
+
+ _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
+ }
+
+ /**
+ * Merges two sets of spacing size presets.
+ *
+ * @since 6.6.0
+ *
+ * @param array $base The base set of spacing sizes.
+ * @param array $incoming The set of spacing sizes to merge with the base. Duplicate slugs will override the base values.
+ * @return array The merged set of spacing sizes.
+ */
+ private static function merge_spacing_sizes( $base, $incoming ) {
+ $merged = array();
+ foreach ( $base as $item ) {
+ $merged[ $item['slug'] ] = $item;
+ }
+ foreach ( $incoming as $item ) {
+ $merged[ $item['slug'] ] = $item;
+ }
+ return array_values( $merged );
+ }
+
+ /**
+ * Generates a set of spacing sizes by starting with a medium size and
+ * applying an operator with an increment value to generate the rest of the
+ * sizes outward from the medium size. The medium slug is '50' with the rest
+ * of the slugs being 10 apart. The generated names use t-shirt sizing.
+ *
+ * Example:
+ *
+ * $spacing_scale = array(
+ * 'steps' => 4,
+ * 'mediumStep' => 16,
+ * 'unit' => 'px',
+ * 'operator' => '+',
+ * 'increment' => 2,
+ * );
+ * $spacing_sizes = static::compute_spacing_sizes( $spacing_scale );
+ * // -> array(
+ * // array( 'name' => 'Small', 'slug' => '40', 'size' => '14px' ),
+ * // array( 'name' => 'Medium', 'slug' => '50', 'size' => '16px' ),
+ * // array( 'name' => 'Large', 'slug' => '60', 'size' => '18px' ),
+ * // array( 'name' => 'X-Large', 'slug' => '70', 'size' => '20px' ),
+ * // )
+ *
+ * @since 6.6.0
+ *
+ * @param array $spacing_scale {
+ * The spacing scale values. All are required.
+ *
+ * @type int $steps The number of steps in the scale. (up to 10 steps are supported.)
+ * @type float $mediumStep The middle value that gets the slug '50'. (For even number of steps, this becomes the first middle value.)
+ * @type string $unit The CSS unit to use for the sizes.
+ * @type string $operator The mathematical operator to apply to generate the other sizes. Either '+' or '*'.
+ * @type float $increment The value used with the operator to generate the other sizes.
+ * }
+ * @return array The spacing sizes presets or an empty array if some spacing scale values are missing or invalid.
+ */
+ private static function compute_spacing_sizes( $spacing_scale ) {
+ /*
+ * This condition is intentionally missing some checks on ranges for the values in order to
+ * keep backwards compatibility with the previous implementation.
+ */
+ if (
+ ! isset( $spacing_scale['steps'] ) ||
+ ! is_numeric( $spacing_scale['steps'] ) ||
+ 0 === $spacing_scale['steps'] ||
+ ! isset( $spacing_scale['mediumStep'] ) ||
+ ! is_numeric( $spacing_scale['mediumStep'] ) ||
+ ! isset( $spacing_scale['unit'] ) ||
+ ! isset( $spacing_scale['operator'] ) ||
+ ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ||
+ ! isset( $spacing_scale['increment'] ) ||
+ ! is_numeric( $spacing_scale['increment'] )
+ ) {
+ return array();
+ }
+
$unit = '%' === $spacing_scale['unit'] ? '%' : sanitize_title( $spacing_scale['unit'] );
$current_step = $spacing_scale['mediumStep'];
$steps_mid_point = round( $spacing_scale['steps'] / 2, 0 );
@@ -3844,14 +4000,7 @@ public function set_spacing_sizes() {
$spacing_sizes[] = $above_sizes_item;
}
- // If there are 7 or less steps in the scale revert to numbers for labels instead of t-shirt sizes.
- if ( $spacing_scale['steps'] <= 7 ) {
- for ( $spacing_sizes_count = 0; $spacing_sizes_count < count( $spacing_sizes ); $spacing_sizes_count++ ) {
- $spacing_sizes[ $spacing_sizes_count ]['name'] = (string) ( $spacing_sizes_count + 1 );
- }
- }
-
- _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes );
+ return $spacing_sizes;
}
/**
diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php
index b21fb956ff8ff..84f999e4e9d02 100644
--- a/lib/class-wp-theme-json-resolver-gutenberg.php
+++ b/lib/class-wp-theme-json-resolver-gutenberg.php
@@ -600,7 +600,6 @@ public static function get_merged_data( $origin = 'custom' ) {
$result = new WP_Theme_JSON_Gutenberg();
$result->merge( static::get_core_data() );
if ( 'default' === $origin ) {
- $result->set_spacing_sizes();
return $result;
}
@@ -611,12 +610,10 @@ public static function get_merged_data( $origin = 'custom' ) {
$result->merge( static::get_theme_data() );
if ( 'theme' === $origin ) {
- $result->set_spacing_sizes();
return $result;
}
$result->merge( static::get_user_data() );
- $result->set_spacing_sizes();
return $result;
}
diff --git a/lib/class-wp-theme-json-schema-gutenberg.php b/lib/class-wp-theme-json-schema-gutenberg.php
index 1eea7ddaa2736..0def88f86a23a 100644
--- a/lib/class-wp-theme-json-schema-gutenberg.php
+++ b/lib/class-wp-theme-json-schema-gutenberg.php
@@ -131,7 +131,7 @@ private static function migrate_v2_to_v3( $old ) {
* affect the generated CSS. And in v2 we provided default font sizes
* when the theme did not provide any.
*/
- if ( isset( $new['settings']['typography']['fontSizes'] ) ) {
+ if ( isset( $old['settings']['typography']['fontSizes'] ) ) {
if ( ! isset( $new['settings'] ) ) {
$new['settings'] = array();
}
@@ -141,6 +141,38 @@ private static function migrate_v2_to_v3( $old ) {
$new['settings']['typography']['defaultFontSizes'] = false;
}
+ /*
+ * Similarly to defaultFontSizes, we need to migrate defaultSpacingSizes
+ * as it controls the PRESETS_METADATA prevent_override which was
+ * previously hardcoded to false. This only needs to happen when the
+ * theme provided spacing sizes via spacingSizes or spacingScale.
+ */
+ if (
+ isset( $old['settings']['spacing']['spacingSizes'] ) ||
+ isset( $old['settings']['spacing']['spacingScale'] )
+ ) {
+ if ( ! isset( $new['settings'] ) ) {
+ $new['settings'] = array();
+ }
+ if ( ! isset( $new['settings']['spacing'] ) ) {
+ $new['settings']['spacing'] = array();
+ }
+ $new['settings']['spacing']['defaultSpacingSizes'] = false;
+ }
+
+ /*
+ * In v3 spacingSizes is merged with the generated spacingScale sizes
+ * instead of completely replacing them. The v3 behavior is what was
+ * documented for the v2 schema, but the code never actually did work
+ * that way. Instead of surprising users with a behavior change two
+ * years after the fact at the same time as a v3 update is introduced,
+ * we'll continue using the "bugged" behavior for v2 themes. And treat
+ * the "bug fix" as a breaking change for v3.
+ */
+ if ( isset( $old['settings']['spacing']['spacingSizes'] ) ) {
+ unset( $new['settings']['spacing']['spacingScale'] );
+ }
+
return $new;
}
diff --git a/lib/compat/wordpress-6.6/blocks.php b/lib/compat/wordpress-6.6/blocks.php
new file mode 100644
index 0000000000000..0d8805a489d9c
--- /dev/null
+++ b/lib/compat/wordpress-6.6/blocks.php
@@ -0,0 +1,46 @@
+ array( 'content' ),
+ 'core/heading' => array( 'content' ),
+ 'core/image' => array( 'id', 'url', 'title', 'alt' ),
+ 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ),
+ );
+
+ $bindings = $parsed_block['attrs']['metadata']['bindings'] ?? array();
+ if (
+ isset( $bindings['__default']['source'] ) &&
+ 'core/pattern-overrides' === $bindings['__default']['source']
+ ) {
+ $updated_bindings = array();
+
+ // Build an binding array of all supported attributes.
+ // Note that this also omits the `__default` attribute from the
+ // resulting array.
+ foreach ( $supported_block_attrs[ $parsed_block['blockName'] ] as $attribute_name ) {
+ // Retain any non-pattern override bindings that might be present.
+ $updated_bindings[ $attribute_name ] = isset( $bindings[ $attribute_name ] )
+ ? $bindings[ $attribute_name ]
+ : array( 'source' => 'core/pattern-overrides' );
+ }
+ $parsed_block['attrs']['metadata']['bindings'] = $updated_bindings;
+ }
+
+ return $parsed_block;
+}
+
+add_filter( 'render_block_data', 'gutenberg_replace_pattern_override_default_binding', 10, 1 );
diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php
index 7ca14e43d715b..a9d5540e56dc5 100644
--- a/lib/experimental/script-modules.php
+++ b/lib/experimental/script-modules.php
@@ -212,7 +212,9 @@ function gutenberg_dequeue_module( $module_identifier ) {
*/
function gutenberg_print_script_module_data(): void {
$get_marked_for_enqueue = new ReflectionMethod( 'WP_Script_Modules', 'get_marked_for_enqueue' );
- $get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' );
+ $get_marked_for_enqueue->setAccessible( true );
+ $get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' );
+ $get_import_map->setAccessible( true );
$modules = array();
foreach ( array_keys( $get_marked_for_enqueue->invoke( wp_script_modules() ) ) as $id ) {
diff --git a/lib/load.php b/lib/load.php
index 6179ade9a2288..23985f9c8a92e 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -131,6 +131,7 @@ function gutenberg_is_experiment_enabled( $name ) {
// WordPress 6.6 compat.
require __DIR__ . '/compat/wordpress-6.6/admin-bar.php';
+require __DIR__ . '/compat/wordpress-6.6/blocks.php';
require __DIR__ . '/compat/wordpress-6.6/compat.php';
require __DIR__ . '/compat/wordpress-6.6/resolve-patterns.php';
require __DIR__ . '/compat/wordpress-6.6/block-bindings/pattern-overrides.php';
diff --git a/lib/theme.json b/lib/theme.json
index 2f1f723bf75f7..f638d9722ef67 100644
--- a/lib/theme.json
+++ b/lib/theme.json
@@ -265,6 +265,7 @@
"margin": false,
"padding": false,
"customSpacingSize": true,
+ "defaultSpacingSizes": true,
"units": [ "px", "em", "rem", "vh", "vw", "%" ],
"spacingScale": {
"operator": "*",
diff --git a/package-lock.json b/package-lock.json
index f78a8f5c48ebd..e0440dfb6a16e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54291,6 +54291,7 @@
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"deepmerge": "^4.3.0",
+ "fast-deep-equal": "^3.1.3",
"is-plain-object": "^5.0.0",
"memize": "^2.1.0",
"react-autosize-textarea": "^7.1.0",
@@ -69409,6 +69410,7 @@
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"deepmerge": "^4.3.0",
+ "fast-deep-equal": "^3.1.3",
"is-plain-object": "^5.0.0",
"memize": "^2.1.0",
"react-autosize-textarea": "^7.1.0",
diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js
index 3122fb5cf0373..8196f7688d4b5 100644
--- a/packages/block-editor/src/components/block-card/index.js
+++ b/packages/block-editor/src/components/block-card/index.js
@@ -7,7 +7,11 @@ import clsx from 'clsx';
* WordPress dependencies
*/
import deprecated from '@wordpress/deprecated';
-import { Button } from '@wordpress/components';
+import {
+ Button,
+ __experimentalText as Text,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
import { chevronLeft, chevronRight } from '@wordpress/icons';
import { __, isRTL } from '@wordpress/i18n';
import { useSelect, useDispatch } from '@wordpress/data';
@@ -60,14 +64,14 @@ function BlockCard( { title, icon, description, blockType, className } ) {
/>
) }
-
+
{ title }
{ description && (
-
+
{ description }
-
+
) }
-
+
);
}
diff --git a/packages/block-editor/src/components/block-card/style.scss b/packages/block-editor/src/components/block-card/style.scss
index 9cd45c7e98296..42cf77aa4b0a8 100644
--- a/packages/block-editor/src/components/block-card/style.scss
+++ b/packages/block-editor/src/components/block-card/style.scss
@@ -5,10 +5,6 @@
padding: $grid-unit-20;
}
-.block-editor-block-card__content {
- flex-grow: 1;
-}
-
.block-editor-block-card__title {
font-weight: 500;
@@ -20,13 +16,6 @@
}
}
-.block-editor-block-card__description {
- display: block;
- font-size: $default-font-size;
- line-height: $default-line-height;
- margin-top: $grid-unit-05;
-}
-
.block-editor-block-card .block-editor-block-icon {
flex: 0 0 $button-size-small;
margin-left: 0;
diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js
index 0220a9877eba2..030d08b42ce4d 100644
--- a/packages/block-editor/src/components/block-list/block.js
+++ b/packages/block-editor/src/components/block-list/block.js
@@ -265,6 +265,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => {
__unstableMarkLastChangeAsPersistent,
moveBlocksToPosition,
removeBlock,
+ selectBlock,
} = dispatch( blockEditorStore );
// Do not add new properties here, use `useDispatch` instead to avoid
@@ -306,6 +307,28 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => {
canInsertBlockType,
} = registry.select( blockEditorStore );
+ function switchToDefaultOrRemove() {
+ const block = getBlock( clientId );
+ const defaultBlockName = getDefaultBlockName();
+ if ( getBlockName( clientId ) !== defaultBlockName ) {
+ const replacement = switchToBlockType(
+ block,
+ defaultBlockName
+ );
+ if ( replacement && replacement.length ) {
+ replaceBlocks( clientId, replacement );
+ }
+ } else if ( isUnmodifiedDefaultBlock( block ) ) {
+ const nextBlockClientId = getNextBlockClientId( clientId );
+ if ( nextBlockClientId ) {
+ registry.batch( () => {
+ removeBlock( clientId );
+ selectBlock( nextBlockClientId );
+ } );
+ }
+ }
+ }
+
/**
* Moves the block with clientId up one level. If the block type
* cannot be inserted at the new location, it will be attempted to
@@ -345,7 +368,16 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => {
getDefaultBlockName()
);
- if ( replacement && replacement.length ) {
+ if (
+ replacement &&
+ replacement.length &&
+ replacement.every( ( block ) =>
+ canInsertBlockType(
+ block.name,
+ targetRootClientId
+ )
+ )
+ ) {
insertBlocks(
replacement,
getBlockIndex( _clientId ),
@@ -353,6 +385,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => {
changeSelection
);
removeBlock( firstClientId, false );
+ } else {
+ switchToDefaultOrRemove();
}
}
@@ -463,16 +497,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => {
}
moveFirstItemUp( rootClientId );
- } else if (
- getBlockName( clientId ) !== getDefaultBlockName()
- ) {
- const replacement = switchToBlockType(
- getBlock( clientId ),
- getDefaultBlockName()
- );
- if ( replacement && replacement.length ) {
- replaceBlocks( clientId, replacement );
- }
+ } else {
+ switchToDefaultOrRemove();
}
}
},
diff --git a/packages/block-editor/src/components/block-patterns-list/index.js b/packages/block-editor/src/components/block-patterns-list/index.js
index 845bfc4f803a3..b2af3456be7b0 100644
--- a/packages/block-editor/src/components/block-patterns-list/index.js
+++ b/packages/block-editor/src/components/block-patterns-list/index.js
@@ -147,7 +147,10 @@ function BlockPattern( {
/>
{ showTitle && (
-
+
{ pattern.type ===
INSERTER_PATTERN_TYPES.user &&
! pattern.syncStatus && (
diff --git a/packages/block-editor/src/components/block-patterns-list/style.scss b/packages/block-editor/src/components/block-patterns-list/style.scss
index 00dc650a3fbbd..6b23c1e844dad 100644
--- a/packages/block-editor/src/components/block-patterns-list/style.scss
+++ b/packages/block-editor/src/components/block-patterns-list/style.scss
@@ -27,8 +27,9 @@
scroll-margin-bottom: ($grid-unit-40 + $grid-unit-30);
.block-editor-block-patterns-list__item-title {
- text-align: left;
flex-grow: 1;
+ font-size: $helptext-font-size;
+ text-align: left;
}
.block-editor-block-preview__container {
@@ -59,7 +60,7 @@
.block-editor-patterns__pattern-details:not(:empty) {
align-items: center;
margin-top: $grid-unit-10;
- margin-bottom: $grid-unit-05; // Add more space as there is a visual label on user-created patterns.
+ padding-bottom: $grid-unit-05; // Add more space for labels on user-created patterns.
}
.block-editor-patterns__pattern-icon-wrapper {
diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js
index 26da27760aa5d..9718545795f7c 100644
--- a/packages/block-editor/src/components/global-styles/dimensions-panel.js
+++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js
@@ -100,14 +100,13 @@ function useHasChildLayout( settings ) {
}
function useHasSpacingPresets( settings ) {
- const {
- custom,
- theme,
- default: defaultPresets,
- } = settings?.spacing?.spacingSizes || {};
- const presets = custom ?? theme ?? defaultPresets ?? [];
-
- return presets.length > 0;
+ const { defaultSpacingSizes, spacingSizes } = settings?.spacing || {};
+ return (
+ ( defaultSpacingSizes !== false &&
+ spacingSizes?.default?.length > 0 ) ||
+ spacingSizes?.theme?.length > 0 ||
+ spacingSizes?.custom?.length > 0
+ );
}
function filterValuesBySides( values, sides ) {
diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js
index 5c1e87001ca84..933d97b0c839e 100644
--- a/packages/block-editor/src/components/global-styles/hooks.js
+++ b/packages/block-editor/src/components/global-styles/hooks.js
@@ -60,6 +60,7 @@ const VALID_SETTINGS = [
'position.fixed',
'position.sticky',
'spacing.customSpacingSize',
+ 'spacing.defaultSpacingSizes',
'spacing.spacingSizes',
'spacing.spacingScale',
'spacing.blockGap',
@@ -76,6 +77,7 @@ const VALID_SETTINGS = [
'typography.fontWeight',
'typography.letterSpacing',
'typography.lineHeight',
+ 'typography.textAlign',
'typography.textColumns',
'typography.textDecoration',
'typography.textTransform',
@@ -377,8 +379,13 @@ export function useSettingsForBlockElement(
? updatedSettings.shadow
: false;
+ // Text alignment is only available for blocks.
+ if ( element ) {
+ updatedSettings.typography.textAlign = false;
+ }
+
return updatedSettings;
- }, [ parentSettings, supportedStyles, supports ] );
+ }, [ parentSettings, supportedStyles, supports, element ] );
}
export function useColorsPerOrigin( settings ) {
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 64a7be0443e1e..3106723945fe8 100644
--- a/packages/block-editor/src/components/global-styles/typography-panel.js
+++ b/packages/block-editor/src/components/global-styles/typography-panel.js
@@ -17,6 +17,7 @@ import FontFamilyControl from '../font-family';
import FontAppearanceControl from '../font-appearance-control';
import LineHeightControl from '../line-height-control';
import LetterSpacingControl from '../letter-spacing-control';
+import TextAlignmentControl from '../text-alignment-control';
import TextTransformControl from '../text-transform-control';
import TextDecorationControl from '../text-decoration-control';
import WritingModeControl from '../writing-mode-control';
@@ -31,6 +32,7 @@ export function useHasTypographyPanel( settings ) {
const hasLineHeight = useHasLineHeightControl( settings );
const hasFontAppearance = useHasAppearanceControl( settings );
const hasLetterSpacing = useHasLetterSpacingControl( settings );
+ const hasTextAlign = useHasTextAlignmentControl( settings );
const hasTextTransform = useHasTextTransformControl( settings );
const hasTextDecoration = useHasTextDecorationControl( settings );
const hasWritingMode = useHasWritingModeControl( settings );
@@ -42,6 +44,7 @@ export function useHasTypographyPanel( settings ) {
hasLineHeight ||
hasFontAppearance ||
hasLetterSpacing ||
+ hasTextAlign ||
hasTextTransform ||
hasFontSize ||
hasTextDecoration ||
@@ -92,6 +95,10 @@ function useHasTextTransformControl( settings ) {
return settings?.typography?.textTransform;
}
+function useHasTextAlignmentControl( settings ) {
+ return settings?.typography?.textAlign;
+}
+
function useHasTextDecorationControl( settings ) {
return settings?.typography?.textDecoration;
}
@@ -151,6 +158,7 @@ const DEFAULT_CONTROLS = {
fontAppearance: true,
lineHeight: true,
letterSpacing: true,
+ textAlign: true,
textTransform: true,
textDecoration: true,
writingMode: true,
@@ -339,6 +347,22 @@ export default function TypographyPanel( {
const hasWritingMode = () => !! value?.typography?.writingMode;
const resetWritingMode = () => setWritingMode( undefined );
+ // Text Alignment
+ const hasTextAlignmentControl = useHasTextAlignmentControl( settings );
+
+ const textAlign = decodeValue( inheritedValue?.typography?.textAlign );
+ const setTextAlign = ( newValue ) => {
+ onChange(
+ setImmutably(
+ value,
+ [ 'typography', 'textAlign' ],
+ newValue || undefined
+ )
+ );
+ };
+ const hasTextAlign = () => !! value?.typography?.textAlign;
+ const resetTextAlign = () => setTextAlign( undefined );
+
const resetAllFilter = useCallback( ( previousValue ) => {
return {
...previousValue,
@@ -519,6 +543,22 @@ export default function TypographyPanel( {
/>
) }
+ { hasTextAlignmentControl && (
+
+
+
+ ) }
);
}
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 6ce2150b9d203..28e1ddac9ab1d 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
@@ -916,7 +916,7 @@ export const toStyles = (
.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }
.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:not(.wp-block-block, .alignfull, .alignwide)) { padding-right: 0; padding-left: 0; }
- .has-global-padding :where(.has-global-padding:not(.wp-block-block, .alignfull, .alignwide)) > .alignfull { margin-left: 0; margin-right: 0; }
+ .has-global-padding :where(.has-global-padding:not(.wp-block-block, .alignfull, .alignwide)) > .alignfull { margin-left: 0; margin-right: 0;
`;
}
diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js
index 9ee57ed7950a8..3703381b23a14 100644
--- a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js
+++ b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js
@@ -136,7 +136,12 @@ export function PatternCategoryPreviews( {
>
-
+
{ category.label }
diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js b/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js
index 7f5abf0b8540e..9e57f00b6b466 100644
--- a/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js
+++ b/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js
@@ -113,7 +113,8 @@ export function PatternsFilter( {
popoverProps={ {
placement: 'right-end',
} }
- label="Filter patterns"
+ label={ __( 'Filter patterns' ) }
+ toggleProps={ { size: 'compact' } }
icon={
}
diff --git a/packages/block-editor/src/components/inserter/block-types-tab.js b/packages/block-editor/src/components/inserter/block-types-tab.js
index 57f66b6e4bb6a..42c4a6a35e14b 100644
--- a/packages/block-editor/src/components/inserter/block-types-tab.js
+++ b/packages/block-editor/src/components/inserter/block-types-tab.js
@@ -3,7 +3,7 @@
*/
import { __, _x } from '@wordpress/i18n';
import { useMemo, useEffect, forwardRef } from '@wordpress/element';
-import { pipe, useAsyncList } from '@wordpress/compose';
+import { useAsyncList } from '@wordpress/compose';
/**
* Internal dependencies
@@ -27,15 +27,15 @@ const MAX_SUGGESTED_ITEMS = 6;
*/
const EMPTY_ARRAY = [];
-export function BlockTypesTab(
- { rootClientId, onInsert, onHover, showMostUsedBlocks },
- ref
-) {
- const [ items, categories, collections, onSelectItem ] = useBlockTypesState(
- rootClientId,
- onInsert
- );
-
+export function BlockTypesTabPanel( {
+ items,
+ collections,
+ categories,
+ onSelectItem,
+ onHover,
+ showMostUsedBlocks,
+ className,
+} ) {
const suggestedItems = useMemo( () => {
return orderBy( items, 'frecency', 'desc' ).slice(
0,
@@ -47,24 +47,6 @@ export function BlockTypesTab(
return items.filter( ( item ) => ! item.category );
}, [ items ] );
- const itemsPerCategory = useMemo( () => {
- return pipe(
- ( itemList ) =>
- itemList.filter(
- ( item ) => item.category && item.category !== 'reusable'
- ),
- ( itemList ) =>
- itemList.reduce( ( acc, item ) => {
- const { category } = item;
- if ( ! acc[ category ] ) {
- acc[ category ] = [];
- }
- acc[ category ].push( item );
- return acc;
- }, {} )
- )( items );
- }, [ items ] );
-
const itemsPerCollection = useMemo( () => {
// Create a new Object to avoid mutating collection.
const result = { ...collections };
@@ -101,14 +83,13 @@ export function BlockTypesTab(
didRenderAllCategories ? collectionEntries : EMPTY_ARRAY
);
- if ( ! items.length ) {
- return ;
- }
-
return (
-
-
- { showMostUsedBlocks && !! suggestedItems.length && (
+
+ { showMostUsedBlocks &&
+ // Only show the most used blocks if the total amount of block
+ // is larger than 1 row, otherwise it is not so useful.
+ items.length > 3 &&
+ !! suggestedItems.length && (
) }
- { currentlyRenderedCategories.map( ( category ) => {
- const categoryItems = itemsPerCategory[ category.slug ];
- if ( ! categoryItems || ! categoryItems.length ) {
+ { currentlyRenderedCategories.map( ( category ) => {
+ const categoryItems = items.filter(
+ ( item ) => item.category === category.slug
+ );
+ if ( ! categoryItems || ! categoryItems.length ) {
+ return null;
+ }
+ return (
+
+
+
+ );
+ } ) }
+
+ { didRenderAllCategories && uncategorizedItems.length > 0 && (
+
+
+
+ ) }
+
+ { currentlyRenderedCollections.map(
+ ( [ namespace, collection ] ) => {
+ const collectionItems = itemsPerCollection[ namespace ];
+ if ( ! collectionItems || ! collectionItems.length ) {
return null;
}
+
return (
);
- } ) }
+ }
+ ) }
+
+ );
+}
- { didRenderAllCategories && uncategorizedItems.length > 0 && (
-
- ;
+ }
+
+ const itemsForCurrentRoot = [];
+ const itemsRemaining = [];
+
+ for ( const item of items ) {
+ // Skip reusable blocks, they moved to the patterns tab.
+ if ( item.category === 'reusable' ) {
+ continue;
+ }
+
+ if ( rootClientId && item.rootClientId === rootClientId ) {
+ itemsForCurrentRoot.push( item );
+ } else {
+ itemsRemaining.push( item );
+ }
+ }
+
+ return (
+
+
+ { !! itemsForCurrentRoot.length && (
+ <>
+
-
- ) }
-
- { currentlyRenderedCollections.map(
- ( [ namespace, collection ] ) => {
- const collectionItems = itemsPerCollection[ namespace ];
- if ( ! collectionItems || ! collectionItems.length ) {
- return null;
- }
-
- return (
-
-
-
- );
- }
+
+ >
) }
+
);
diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js
index 566d0476fbd0f..6b9e694c1cdf8 100644
--- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js
+++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js
@@ -14,20 +14,24 @@ import { useCallback } from '@wordpress/element';
* Internal dependencies
*/
import { store as blockEditorStore } from '../../../store';
+import { withRootClientIdOptionKey } from '../../../store/utils';
/**
* Retrieves the block types inserter state.
*
* @param {string=} rootClientId Insertion's root client ID.
* @param {Function} onInsert function called when inserter a list of blocks.
+ * @param {boolean} isQuick
* @return {Array} Returns the block types state. (block types, categories, collections, onSelect handler)
*/
-const useBlockTypesState = ( rootClientId, onInsert ) => {
+const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => {
const [ items ] = useSelect(
( select ) => [
- select( blockEditorStore ).getInserterItems( rootClientId ),
+ select( blockEditorStore ).getInserterItems( rootClientId, {
+ [ withRootClientIdOptionKey ]: ! isQuick,
+ } ),
],
- [ rootClientId ]
+ [ rootClientId, isQuick ]
);
const [ categories, collections ] = useSelect( ( select ) => {
@@ -37,7 +41,14 @@ const useBlockTypesState = ( rootClientId, onInsert ) => {
const onSelectItem = useCallback(
(
- { name, initialAttributes, innerBlocks, syncStatus, content },
+ {
+ name,
+ initialAttributes,
+ innerBlocks,
+ syncStatus,
+ content,
+ rootClientId: _rootClientId,
+ },
shouldFocusBlock
) => {
const insertedBlock =
@@ -51,7 +62,12 @@ const useBlockTypesState = ( rootClientId, onInsert ) => {
createBlocksFromInnerBlocksTemplate( innerBlocks )
);
- onInsert( insertedBlock, undefined, shouldFocusBlock );
+ onInsert(
+ insertedBlock,
+ undefined,
+ shouldFocusBlock,
+ _rootClientId
+ );
},
[ onInsert ]
);
diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
index 0dae090578ab4..24074ec500456 100644
--- a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
+++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { useDispatch, useSelect } from '@wordpress/data';
+import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
import { isUnmodifiedDefaultBlock } from '@wordpress/blocks';
import { _n, sprintf } from '@wordpress/i18n';
import { speak } from '@wordpress/a11y';
@@ -13,6 +13,34 @@ import { useCallback } from '@wordpress/element';
import { store as blockEditorStore } from '../../../store';
import { unlock } from '../../../lock-unlock';
+function getIndex( {
+ destinationRootClientId,
+ destinationIndex,
+ rootClientId,
+ registry,
+} ) {
+ if ( rootClientId === destinationRootClientId ) {
+ return destinationIndex;
+ }
+ const parents = [
+ '',
+ ...registry
+ .select( blockEditorStore )
+ .getBlockParents( destinationRootClientId ),
+ destinationRootClientId,
+ ];
+ const parentIndex = parents.indexOf( rootClientId );
+ if ( parentIndex !== -1 ) {
+ return (
+ registry
+ .select( blockEditorStore )
+ .getBlockIndex( parents[ parentIndex + 1 ] ) + 1
+ );
+ }
+ return registry.select( blockEditorStore ).getBlockOrder( rootClientId )
+ .length;
+}
+
/**
* @typedef WPInserterConfig
*
@@ -42,6 +70,7 @@ function useInsertionPoint( {
shouldFocusBlock = true,
selectBlockOnInsert = true,
} ) {
+ const registry = useRegistry();
const { getSelectedBlock } = useSelect( blockEditorStore );
const { destinationRootClientId, destinationIndex } = useSelect(
( select ) => {
@@ -91,7 +120,7 @@ function useInsertionPoint( {
} = unlock( useDispatch( blockEditorStore ) );
const onInsertBlocks = useCallback(
- ( blocks, meta, shouldForceFocusBlock = false ) => {
+ ( blocks, meta, shouldForceFocusBlock = false, _rootClientId ) => {
// When we are trying to move focus or select a new block on insert, we also
// need to clear the last focus to avoid the focus being set to the wrong block
// when tabbing back into the canvas if the block was added from outside the
@@ -121,8 +150,17 @@ function useInsertionPoint( {
} else {
insertBlocks(
blocks,
- destinationIndex,
- destinationRootClientId,
+ isAppender || _rootClientId === undefined
+ ? destinationIndex
+ : getIndex( {
+ destinationRootClientId,
+ destinationIndex,
+ rootClientId: _rootClientId,
+ registry,
+ } ),
+ isAppender || _rootClientId === undefined
+ ? destinationRootClientId
+ : _rootClientId,
selectBlockOnInsert,
shouldFocusBlock || shouldForceFocusBlock ? 0 : null,
meta
@@ -154,9 +192,17 @@ function useInsertionPoint( {
);
const onToggleInsertionPoint = useCallback(
- ( show ) => {
- if ( show ) {
- showInsertionPoint( destinationRootClientId, destinationIndex );
+ ( item ) => {
+ if ( item?.hasOwnProperty( 'rootClientId' ) ) {
+ showInsertionPoint(
+ item.rootClientId,
+ getIndex( {
+ destinationRootClientId,
+ destinationIndex,
+ rootClientId: item.rootClientId,
+ registry,
+ } )
+ );
} else {
hideInsertionPoint();
}
diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js
index 3abaee330ed22..6a4ac798b7490 100644
--- a/packages/block-editor/src/components/inserter/menu.js
+++ b/packages/block-editor/src/components/inserter/menu.js
@@ -81,8 +81,13 @@ function InserterMenu(
const blockTypesTabRef = useRef();
const onInsert = useCallback(
- ( blocks, meta, shouldForceFocusBlock ) => {
- onInsertBlocks( blocks, meta, shouldForceFocusBlock );
+ ( blocks, meta, shouldForceFocusBlock, _rootClientId ) => {
+ onInsertBlocks(
+ blocks,
+ meta,
+ shouldForceFocusBlock,
+ _rootClientId
+ );
onSelect();
// Check for focus loss due to filtering blocks by selected block type
@@ -111,7 +116,7 @@ function InserterMenu(
const onHover = useCallback(
( item ) => {
- onToggleInsertionPoint( !! item );
+ onToggleInsertionPoint( item );
setHoveredItem( item );
},
[ onToggleInsertionPoint, setHoveredItem ]
diff --git a/packages/block-editor/src/components/inserter/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js
index 3405ac98b881c..022957df952ce 100644
--- a/packages/block-editor/src/components/inserter/quick-inserter.js
+++ b/packages/block-editor/src/components/inserter/quick-inserter.js
@@ -44,7 +44,8 @@ export default function QuickInserter( {
} );
const [ blockTypes ] = useBlockTypesState(
destinationRootClientId,
- onInsertBlocks
+ onInsertBlocks,
+ true
);
const [ patterns ] = usePatternsState(
@@ -126,6 +127,7 @@ export default function QuickInserter( {
isDraggable={ false }
prioritizePatterns={ prioritizePatterns }
selectBlockOnInsert={ selectBlockOnInsert }
+ isQuick
/>
diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js
index edd99609ea916..9c001823745e6 100644
--- a/packages/block-editor/src/components/inserter/search-results.js
+++ b/packages/block-editor/src/components/inserter/search-results.js
@@ -50,6 +50,7 @@ function InserterSearchResults( {
shouldFocusBlock = true,
prioritizePatterns,
selectBlockOnInsert,
+ isQuick,
} ) {
const debouncedSpeak = useDebounce( speak, 500 );
@@ -80,7 +81,7 @@ function InserterSearchResults( {
blockTypeCategories,
blockTypeCollections,
onSelectBlockType,
- ] = useBlockTypesState( destinationRootClientId, onInsertBlocks );
+ ] = useBlockTypesState( destinationRootClientId, onInsertBlocks, isQuick );
const [ patterns, , onClickPattern ] = usePatternsState(
onInsertBlocks,
destinationRootClientId
diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss
index 7618d9712ad53..4fac1d7ea1f30 100644
--- a/packages/block-editor/src/components/inserter/style.scss
+++ b/packages/block-editor/src/components/inserter/style.scss
@@ -329,18 +329,20 @@ $block-inserter-tabs-height: 44px;
.block-editor-inserter__category-panel {
background: $gray-100;
- border-left: $border-width solid $gray-200;
- border-right: $border-width solid $gray-200;
+ border-top: $border-width solid $gray-200;
+ box-shadow: $border-width $border-width 0 0 rgba($color: #000, $alpha: 0.133); // 0.133 = $gray-200 but with alpha.
+ outline: 1px solid transparent; // Shown for Windows 10 High Contrast mode.
position: absolute;
- top: 0;
+ top: -$border-width;
left: 0;
- height: 100%;
+ height: calc(100% + #{$border-width});
width: 100%;
padding: 0 $grid-unit-20;
display: flex;
flex-direction: column;
@include break-medium {
+ border-left: $border-width solid $gray-200;
padding: 0;
left: 100%;
width: 300px;
@@ -525,8 +527,8 @@ $block-inserter-tabs-height: 44px;
}
}
-.block-editor-inserter__patterns-category-panel-title {
- font-size: calc(1.25 * 13px);
+.components-heading.block-editor-inserter__patterns-category-panel-title {
+ font-weight: 500;
}
.block-editor-inserter__patterns-explore-button,
diff --git a/packages/block-editor/src/components/publish-date-time-picker/index.js b/packages/block-editor/src/components/publish-date-time-picker/index.js
index 57b6428ccd4b0..eeaa5b2daad6f 100644
--- a/packages/block-editor/src/components/publish-date-time-picker/index.js
+++ b/packages/block-editor/src/components/publish-date-time-picker/index.js
@@ -17,10 +17,18 @@ export function PublishDateTimePicker(
onChange,
showPopoverHeaderActions,
isCompact,
+ currentDate,
...additionalProps
},
ref
) {
+ const datePickerProps = {
+ startOfWeek: getSettings().l10n.startOfWeek,
+ onChange,
+ currentDate: isCompact ? undefined : currentDate,
+ currentTime: isCompact ? currentDate : undefined,
+ ...additionalProps,
+ };
const DatePickerComponent = isCompact ? TimePicker : DateTimePicker;
return (
@@ -38,11 +46,7 @@ export function PublishDateTimePicker(
}
onClose={ onClose }
/>
-
+
);
}
diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js
index 2a0afcf24f0dd..cc22c9b804130 100644
--- a/packages/block-editor/src/components/rich-text/index.js
+++ b/packages/block-editor/src/components/rich-text/index.js
@@ -11,6 +11,7 @@ import {
useCallback,
forwardRef,
createContext,
+ useContext,
} from '@wordpress/element';
import { useDispatch, useRegistry, useSelect } from '@wordpress/data';
import { useMergeRefs, useInstanceId } from '@wordpress/compose';
@@ -39,6 +40,7 @@ import { Content, valueToHTMLString } from './content';
import { withDeprecations } from './with-deprecations';
import { unlock } from '../../lock-unlock';
import { canBindBlock } from '../../hooks/use-bindings-attributes';
+import BlockContext from '../block-context';
export const keyboardShortcutContext = createContext();
export const inputEventContext = createContext();
@@ -121,6 +123,7 @@ export function RichTextWrapper(
const context = useBlockEditContext();
const { clientId, isSelected: isBlockSelected, name: blockName } = context;
const blockBindings = context[ blockBindingsKey ];
+ const blockContext = useContext( BlockContext );
const selector = ( select ) => {
// Avoid subscribing to the block editor store if the block is not
// selected.
@@ -170,7 +173,7 @@ export function RichTextWrapper(
const { getBlockBindingsSource } = unlock(
select( blocksStore )
);
- for ( const [ attribute, args ] of Object.entries(
+ for ( const [ attribute, binding ] of Object.entries(
blockBindings
) ) {
if (
@@ -180,13 +183,16 @@ export function RichTextWrapper(
break;
}
- // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it.
+ // If the source is not defined, or if its value of `canUserEditValue` is `false`, disable it.
const blockBindingsSource = getBlockBindingsSource(
- args.source
+ binding.source
);
if (
- ! blockBindingsSource ||
- blockBindingsSource.lockAttributesEditing()
+ ! blockBindingsSource?.canUserEditValue( {
+ select,
+ context: blockContext,
+ args: binding.args,
+ } )
) {
_disableBoundBlocks = true;
break;
diff --git a/packages/block-editor/src/components/spacing-sizes-control/hooks/use-spacing-sizes.js b/packages/block-editor/src/components/spacing-sizes-control/hooks/use-spacing-sizes.js
index e75f3519a1ca6..fcd4e3fb964a6 100644
--- a/packages/block-editor/src/components/spacing-sizes-control/hooks/use-spacing-sizes.js
+++ b/packages/block-editor/src/components/spacing-sizes-control/hooks/use-spacing-sizes.js
@@ -1,28 +1,60 @@
/**
* WordPress dependencies
*/
+import { useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { useSettings } from '../../use-settings';
+import { RANGE_CONTROL_MAX_SIZE } from '../utils';
+
+const EMPTY_ARRAY = [];
+
+const compare = new Intl.Collator( 'und', { numeric: true } ).compare;
export default function useSpacingSizes() {
- const spacingSizes = [ { name: 0, slug: '0', size: 0 } ];
-
- const [ settingsSizes ] = useSettings( 'spacing.spacingSizes' );
- if ( settingsSizes ) {
- spacingSizes.push( ...settingsSizes );
- }
-
- if ( spacingSizes.length > 8 ) {
- spacingSizes.unshift( {
- name: __( 'Default' ),
- slug: 'default',
- size: undefined,
- } );
- }
-
- return spacingSizes;
+ const [
+ customSpacingSizes,
+ themeSpacingSizes,
+ defaultSpacingSizes,
+ defaultSpacingSizesEnabled,
+ ] = useSettings(
+ 'spacing.spacingSizes.custom',
+ 'spacing.spacingSizes.theme',
+ 'spacing.spacingSizes.default',
+ 'spacing.defaultSpacingSizes'
+ );
+
+ const customSizes = customSpacingSizes ?? EMPTY_ARRAY;
+
+ const themeSizes = themeSpacingSizes ?? EMPTY_ARRAY;
+
+ const defaultSizes =
+ defaultSpacingSizes && defaultSpacingSizesEnabled !== false
+ ? defaultSpacingSizes
+ : EMPTY_ARRAY;
+
+ return useMemo( () => {
+ const sizes = [
+ { name: __( 'None' ), slug: '0', size: 0 },
+ ...customSizes,
+ ...themeSizes,
+ ...defaultSizes,
+ ].sort( ( a, b ) => compare( a.slug, b.slug ) );
+
+ return sizes.length > RANGE_CONTROL_MAX_SIZE
+ ? [
+ {
+ name: __( 'Default' ),
+ slug: 'default',
+ size: undefined,
+ },
+ ...sizes,
+ ]
+ : // See https://github.com/WordPress/gutenberg/pull/44247 for reasoning
+ // to use the index as the name in the range control.
+ sizes.map( ( { slug, size }, i ) => ( { name: i, slug, size } ) );
+ }, [ customSizes, themeSizes, defaultSizes ] );
}
diff --git a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js
index eacdda5927f2d..58a81d8b130a3 100644
--- a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js
+++ b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js
@@ -23,6 +23,7 @@ import { settings } from '@wordpress/icons';
import { useSettings } from '../../use-settings';
import { store as blockEditorStore } from '../../../store';
import {
+ RANGE_CONTROL_MAX_SIZE,
ALL_SIDES,
LABELS,
getSliderValueFromPreset,
@@ -79,7 +80,7 @@ export default function SpacingInputControl( {
value = getPresetValueFromCustomValue( value, spacingSizes );
let selectListSizes = spacingSizes;
- const showRangeControl = spacingSizes.length <= 8;
+ const showRangeControl = spacingSizes.length <= RANGE_CONTROL_MAX_SIZE;
const disableCustomSpacingSizes = useSelect( ( select ) => {
const editorSettings = select( blockEditorStore ).getSettings();
diff --git a/packages/block-editor/src/components/spacing-sizes-control/utils.js b/packages/block-editor/src/components/spacing-sizes-control/utils.js
index 32f0dbc59ac46..91c5a91934f6e 100644
--- a/packages/block-editor/src/components/spacing-sizes-control/utils.js
+++ b/packages/block-editor/src/components/spacing-sizes-control/utils.js
@@ -12,6 +12,8 @@ import {
sidesVertical,
} from '@wordpress/icons';
+export const RANGE_CONTROL_MAX_SIZE = 8;
+
export const ALL_SIDES = [ 'top', 'right', 'bottom', 'left' ];
export const DEFAULT_VALUES = {
diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js
index 5e6a9486fcd66..ab12d5d73f0b6 100644
--- a/packages/block-editor/src/hooks/block-bindings.js
+++ b/packages/block-editor/src/hooks/block-bindings.js
@@ -14,10 +14,11 @@ import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
+import { canBindAttribute } from '../hooks/use-bindings-attributes';
import { unlock } from '../lock-unlock';
import InspectorControls from '../components/inspector-controls';
-export const BlockBindingsPanel = ( { metadata } ) => {
+export const BlockBindingsPanel = ( { name, metadata } ) => {
const { bindings } = metadata || {};
const { sources } = useSelect( ( select ) => {
const _sources = unlock(
@@ -33,11 +34,15 @@ export const BlockBindingsPanel = ( { metadata } ) => {
return null;
}
+ // Don't show not allowed attributes.
// Don't show the bindings connected to pattern overrides in the inspectors panel.
// TODO: Explore if this should be abstracted to let other sources decide.
const filteredBindings = { ...bindings };
Object.keys( filteredBindings ).forEach( ( key ) => {
- if ( filteredBindings[ key ].source === 'core/pattern-overrides' ) {
+ if (
+ ! canBindAttribute( name, key ) ||
+ filteredBindings[ key ].source === 'core/pattern-overrides'
+ ) {
delete filteredBindings[ key ];
}
} );
diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js
index dad62bc0594a7..4cd3335022454 100644
--- a/packages/block-editor/src/hooks/style.js
+++ b/packages/block-editor/src/hooks/style.js
@@ -332,7 +332,15 @@ function BlockStyleControls( {
clientId,
name,
setAttributes,
- settings,
+ settings: {
+ ...settings,
+ typography: {
+ ...settings.typography,
+ // The text alignment UI for individual blocks is rendered in
+ // the block toolbar, so disable it here.
+ textAlign: false,
+ },
+ },
};
if ( blockEditingMode !== 'default' ) {
return null;
diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js
index b7a4ca0379dd1..334c751bc01b0 100644
--- a/packages/block-editor/src/hooks/use-bindings-attributes.js
+++ b/packages/block-editor/src/hooks/use-bindings-attributes.js
@@ -4,7 +4,7 @@
import { store as blocksStore } from '@wordpress/blocks';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useRegistry, useSelect } from '@wordpress/data';
-import { useCallback } from '@wordpress/element';
+import { useCallback, useMemo } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
/**
@@ -29,6 +29,41 @@ const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
'core/button': [ 'url', 'text', 'linkTarget', 'rel' ],
};
+const DEFAULT_ATTRIBUTE = '__default';
+
+/**
+ * Returns the bindings with the `__default` binding for pattern overrides
+ * replaced with the full-set of supported attributes. e.g.:
+ *
+ * bindings passed in: `{ __default: { source: 'core/pattern-overrides' } }`
+ * bindings returned: `{ content: { source: 'core/pattern-overrides' } }`
+ *
+ * @param {string} blockName The block name (e.g. 'core/paragraph').
+ * @param {Object} bindings A block's bindings from the metadata attribute.
+ *
+ * @return {Object} The bindings with default replaced for pattern overrides.
+ */
+function replacePatternOverrideDefaultBindings( blockName, bindings ) {
+ // The `__default` binding currently only works for pattern overrides.
+ if (
+ bindings?.[ DEFAULT_ATTRIBUTE ]?.source === 'core/pattern-overrides'
+ ) {
+ const supportedAttributes = BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
+ const bindingsWithDefaults = {};
+ for ( const attributeName of supportedAttributes ) {
+ // If the block has mixed binding sources, retain any non pattern override bindings.
+ const bindingSource = bindings[ attributeName ]
+ ? bindings[ attributeName ]
+ : { source: 'core/pattern-overrides' };
+ bindingsWithDefaults[ attributeName ] = bindingSource;
+ }
+
+ return bindingsWithDefaults;
+ }
+
+ return bindings;
+}
+
/**
* Based on the given block name,
* check if it is possible to bind the block.
@@ -61,8 +96,15 @@ export const withBlockBindingSupport = createHigherOrderComponent(
const sources = useSelect( ( select ) =>
unlock( select( blocksStore ) ).getAllBlockBindingsSources()
);
- const bindings = props.attributes.metadata?.bindings;
const { name, clientId, context } = props;
+ const bindings = useMemo(
+ () =>
+ replacePatternOverrideDefaultBindings(
+ name,
+ props.attributes.metadata?.bindings
+ ),
+ [ props.attributes.metadata?.bindings, name ]
+ );
const boundAttributes = useSelect( () => {
if ( ! bindings ) {
return;
@@ -128,8 +170,8 @@ export const withBlockBindingSupport = createHigherOrderComponent(
continue;
}
- const source =
- sources[ bindings[ attributeName ].source ];
+ const binding = bindings[ attributeName ];
+ const source = sources[ binding?.source ];
if ( ! source?.setValue && ! source?.setValues ) {
continue;
}
@@ -157,12 +199,13 @@ export const withBlockBindingSupport = createHigherOrderComponent(
attributeName,
value,
] of Object.entries( attributes ) ) {
+ const binding = bindings[ attributeName ];
source.setValue( {
registry,
context,
clientId,
attributeName,
- args: bindings[ attributeName ].args,
+ args: binding.args,
value,
} );
}
diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js
index c14b6329cf2ec..4338262300c61 100644
--- a/packages/block-editor/src/hooks/utils.js
+++ b/packages/block-editor/src/hooks/utils.js
@@ -240,7 +240,11 @@ export function useBlockSettings( name, parentLayout ) {
padding,
margin,
blockGap,
- spacingSizes,
+ defaultSpacingSizesEnabled,
+ customSpacingSize,
+ userSpacingSizes,
+ defaultSpacingSizes,
+ themeSpacingSizes,
units,
aspectRatio,
minHeight,
@@ -293,7 +297,11 @@ export function useBlockSettings( name, parentLayout ) {
'spacing.padding',
'spacing.margin',
'spacing.blockGap',
- 'spacing.spacingSizes',
+ 'spacing.defaultSpacingSizes',
+ 'spacing.customSpacingSize',
+ 'spacing.spacingSizes.custom',
+ 'spacing.spacingSizes.default',
+ 'spacing.spacingSizes.theme',
'spacing.units',
'dimensions.aspectRatio',
'dimensions.minHeight',
@@ -384,8 +392,12 @@ export function useBlockSettings( name, parentLayout ) {
},
spacing: {
spacingSizes: {
- custom: spacingSizes,
+ custom: userSpacingSizes,
+ default: defaultSpacingSizes,
+ theme: themeSpacingSizes,
},
+ customSpacingSize,
+ defaultSpacingSizes: defaultSpacingSizesEnabled,
padding,
margin,
blockGap,
@@ -428,7 +440,11 @@ export function useBlockSettings( name, parentLayout ) {
padding,
margin,
blockGap,
- spacingSizes,
+ defaultSpacingSizesEnabled,
+ customSpacingSize,
+ userSpacingSizes,
+ defaultSpacingSizes,
+ themeSpacingSizes,
units,
aspectRatio,
minHeight,
diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js
index 006ec42987a01..848a3ee49251b 100644
--- a/packages/block-editor/src/private-apis.js
+++ b/packages/block-editor/src/private-apis.js
@@ -42,6 +42,7 @@ import { PrivateRichText } from './components/rich-text/';
import { PrivateBlockPopover } from './components/block-popover';
import { PrivateInserterLibrary } from './components/inserter/library';
import { PrivatePublishDateTimePicker } from './components/publish-date-time-picker';
+import useSpacingSizes from './components/spacing-sizes-control/hooks/use-spacing-sizes';
/**
* Private @wordpress/block-editor APIs.
@@ -84,4 +85,5 @@ lock( privateApis, {
reusableBlocksSelectKey,
PrivateBlockPopover,
PrivatePublishDateTimePicker,
+ useSpacingSizes,
} );
diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js
index 74814cf22746b..db2c615dd5d6c 100644
--- a/packages/block-editor/src/store/actions.js
+++ b/packages/block-editor/src/store/actions.js
@@ -18,6 +18,7 @@ import {
} from '@wordpress/blocks';
import { speak } from '@wordpress/a11y';
import { __, _n, sprintf } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
import { create, insert, remove, toHTMLString } from '@wordpress/rich-text';
import deprecated from '@wordpress/deprecated';
@@ -872,6 +873,30 @@ export const __unstableSplitSelection =
typeof selectionB.attributeKey === 'string'
? selectionB.attributeKey
: findRichTextAttributeKey( blockBType );
+ const blockAttributes = select.getBlockAttributes(
+ selectionA.clientId
+ );
+ const bindings = blockAttributes?.metadata?.bindings;
+
+ // If the attribute is bound, don't split the selection and insert a new block instead.
+ if ( bindings?.[ attributeKeyA ] ) {
+ // Show warning if user tries to insert a block into another block with bindings.
+ if ( blocks.length ) {
+ const { createWarningNotice } =
+ registry.dispatch( noticesStore );
+ createWarningNotice(
+ __(
+ "Blocks can't be inserted into other blocks with bindings"
+ ),
+ {
+ type: 'snackbar',
+ }
+ );
+ return;
+ }
+ dispatch.insertAfterBlock( selectionA.clientId );
+ return;
+ }
// Can't split if the selection is not set.
if (
@@ -918,9 +943,7 @@ export const __unstableSplitSelection =
);
}
- const length = select.getBlockAttributes( selectionA.clientId )[
- attributeKeyA
- ].length;
+ const length = blockAttributes[ attributeKeyA ].length;
if ( selectionA.offset === 0 && length ) {
dispatch.insertBlocks(
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index 1c685eb4230ba..bf7b5125a770e 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -22,6 +22,7 @@ import { createSelector, createRegistrySelector } from '@wordpress/data';
* Internal dependencies
*/
import {
+ withRootClientIdOptionKey,
checkAllowListRecursive,
checkAllowList,
getAllPatternsDependants,
@@ -1995,7 +1996,7 @@ const buildBlockTypeItem =
*/
export const getInserterItems = createRegistrySelector( ( select ) =>
createSelector(
- ( state, rootClientId = null ) => {
+ ( state, rootClientId = null, options = {} ) => {
const buildReusableBlockInserterItem = ( reusableBlock ) => {
const icon = ! reusableBlock.wp_pattern_sync_status
? {
@@ -2037,16 +2038,73 @@ export const getInserterItems = createRegistrySelector( ( select ) =>
buildScope: 'inserter',
} );
- const blockTypeInserterItems = getBlockTypes()
+ let blockTypeInserterItems = getBlockTypes()
.filter( ( blockType ) =>
- canIncludeBlockTypeInInserter(
- state,
- blockType,
- rootClientId
- )
+ hasBlockSupport( blockType, 'inserter', true )
)
.map( buildBlockTypeInserterItem );
+ if ( options[ withRootClientIdOptionKey ] ) {
+ blockTypeInserterItems = blockTypeInserterItems.reduce(
+ ( accumulator, item ) => {
+ item.rootClientId = rootClientId ?? '';
+
+ while (
+ ! canInsertBlockTypeUnmemoized(
+ state,
+ item.name,
+ item.rootClientId
+ )
+ ) {
+ if ( ! item.rootClientId ) {
+ let sectionRootClientId;
+ try {
+ sectionRootClientId = unlock(
+ getSettings( state )
+ ).sectionRootClientId;
+ } catch ( e ) {}
+ if (
+ sectionRootClientId &&
+ canInsertBlockTypeUnmemoized(
+ state,
+ item.name,
+ sectionRootClientId
+ )
+ ) {
+ item.rootClientId = sectionRootClientId;
+ } else {
+ delete item.rootClientId;
+ }
+ break;
+ } else {
+ const parentClientId = getBlockRootClientId(
+ state,
+ item.rootClientId
+ );
+ item.rootClientId = parentClientId;
+ }
+ }
+
+ // We could also add non insertable items and gray them out.
+ if ( item.hasOwnProperty( 'rootClientId' ) ) {
+ accumulator.push( item );
+ }
+
+ return accumulator;
+ },
+ []
+ );
+ } else {
+ blockTypeInserterItems = blockTypeInserterItems.filter(
+ ( blockType ) =>
+ canIncludeBlockTypeInInserter(
+ state,
+ blockType,
+ rootClientId
+ )
+ );
+ }
+
const items = blockTypeInserterItems.reduce(
( accumulator, item ) => {
const { variations = [] } = item;
diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js
index f236c4a7e56eb..c94453e99c60a 100644
--- a/packages/block-editor/src/store/utils.js
+++ b/packages/block-editor/src/store/utils.js
@@ -5,6 +5,8 @@ import { selectBlockPatternsKey } from './private-keys';
import { unlock } from '../lock-unlock';
import { STORE_NAME } from './constants';
+export const withRootClientIdOptionKey = Symbol( 'withRootClientId' );
+
export const checkAllowList = ( list, item, defaultResult = null ) => {
if ( typeof list === 'boolean' ) {
return list;
diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js
index a4f054db46665..8d4147cb31855 100644
--- a/packages/block-library/src/block/edit.js
+++ b/packages/block-library/src/block/edit.js
@@ -40,7 +40,8 @@ import { name as patternBlockName } from './index';
import { unlock } from '../lock-unlock';
const { useLayoutClasses } = unlock( blockEditorPrivateApis );
-const { isOverridableBlock } = unlock( patternsPrivateApis );
+const { isOverridableBlock, hasOverridableBlocks } =
+ unlock( patternsPrivateApis );
const fullAlignments = [ 'full', 'wide', 'left', 'right' ];
@@ -73,15 +74,6 @@ const useInferredLayout = ( blocks, parentLayout ) => {
}, [ blocks, parentLayout ] );
};
-function hasOverridableBlocks( blocks ) {
- return blocks.some( ( block ) => {
- if ( isOverridableBlock( block ) ) {
- return true;
- }
- return hasOverridableBlocks( block.innerBlocks );
- } );
-}
-
function setBlockEditMode( setEditMode, blocks, mode ) {
blocks.forEach( ( block ) => {
const editMode =
diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php
index 9403205c18659..8beef975fad6f 100644
--- a/packages/block-library/src/block/index.php
+++ b/packages/block-library/src/block/index.php
@@ -78,7 +78,7 @@ function render_block_core_block( $attributes ) {
* filter so that it is available when a pattern's inner blocks are
* rendering via do_blocks given it only receives the inner content.
*/
- $has_pattern_overrides = isset( $attributes['content'] );
+ $has_pattern_overrides = isset( $attributes['content'] ) && null !== get_block_bindings_source( 'core/pattern-overrides' );
if ( $has_pattern_overrides ) {
$filter_block_context = static function ( $context ) use ( $attributes ) {
$context['pattern/overrides'] = $attributes['content'];
diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js
index b6c4095ca82a6..e5bd5e6b5f064 100644
--- a/packages/block-library/src/button/edit.js
+++ b/packages/block-library/src/button/edit.js
@@ -156,6 +156,7 @@ function ButtonEdit( props ) {
onReplace,
mergeBlocks,
clientId,
+ context,
} = props;
const {
tagName,
@@ -246,8 +247,11 @@ function ButtonEdit( props ) {
return {
lockUrlControls:
!! metadata?.bindings?.url &&
- ( ! blockBindingsSource ||
- blockBindingsSource?.lockAttributesEditing() ),
+ ! blockBindingsSource?.canUserEditValue( {
+ select,
+ context,
+ args: metadata?.bindings?.url?.args,
+ } ),
};
},
[ isSelected, metadata?.bindings?.url ]
diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js
index 6ff392555a93a..673ab44a8c28a 100644
--- a/packages/block-library/src/image/edit.js
+++ b/packages/block-library/src/image/edit.js
@@ -317,8 +317,11 @@ export function ImageEdit( {
return {
lockUrlControls:
!! metadata?.bindings?.url &&
- ( ! blockBindingsSource ||
- blockBindingsSource?.lockAttributesEditing() ),
+ ! blockBindingsSource?.canUserEditValue( {
+ select,
+ context,
+ args: metadata?.bindings?.url?.args,
+ } ),
lockUrlControlsMessage: blockBindingsSource?.label
? sprintf(
/* translators: %s: Label of the bindings source. */
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index f81d9c06621e6..6095ef2fee24d 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -462,8 +462,11 @@ export default function Image( {
return {
lockUrlControls:
!! urlBinding &&
- ( ! urlBindingSource ||
- urlBindingSource?.lockAttributesEditing() ),
+ ! urlBindingSource?.canUserEditValue( {
+ select,
+ context,
+ args: urlBinding?.args,
+ } ),
lockHrefControls:
// Disable editing the link of the URL if the image is inside a pattern instance.
// This is a temporary solution until we support overriding the link on the frontend.
@@ -474,8 +477,11 @@ export default function Image( {
hasParentPattern,
lockAltControls:
!! altBinding &&
- ( ! altBindingSource ||
- altBindingSource?.lockAttributesEditing() ),
+ ! altBindingSource?.canUserEditValue( {
+ select,
+ context,
+ args: altBinding?.args,
+ } ),
lockAltControlsMessage: altBindingSource?.label
? sprintf(
/* translators: %s: Label of the bindings source. */
@@ -485,8 +491,11 @@ export default function Image( {
: __( 'Connected to dynamic data' ),
lockTitleControls:
!! titleBinding &&
- ( ! titleBindingSource ||
- titleBindingSource?.lockAttributesEditing() ),
+ ! titleBindingSource?.canUserEditValue( {
+ select,
+ context,
+ args: titleBinding?.args,
+ } ),
lockTitleControlsMessage: titleBindingSource?.label
? sprintf(
/* translators: %s: Label of the bindings source. */
diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js
index 8e011505296eb..bcdbc658507fc 100644
--- a/packages/block-library/src/media-text/edit.js
+++ b/packages/block-library/src/media-text/edit.js
@@ -187,6 +187,12 @@ function MediaTextEdit( {
mediaId: undefined,
mediaUrl: undefined,
mediaAlt: undefined,
+ mediaLink: undefined,
+ linkDestination: undefined,
+ linkTarget: undefined,
+ linkClass: undefined,
+ rel: undefined,
+ href: undefined,
useFeaturedImage: ! useFeaturedImage,
} );
};
@@ -453,17 +459,13 @@ function MediaTextEdit( {
>
) }
- { mediaType === 'image' && (
+ { mediaType === 'image' && ! useFeaturedImage && (
{
// If there is no link then remove the auto-inserted block.
// This avoids empty blocks which can provided a poor UX.
if ( ! url ) {
- // Select the previous block to keep focus nearby
- selectPreviousBlock( clientId, true );
+ // Fixes https://github.com/WordPress/gutenberg/issues/61361
+ // There's a chance we're closing due to the user selecting the browse all button.
+ // Only move focus if the focus is still within the popover ui. If it's not within
+ // the popover, it's because something has taken the focus from the popover, and
+ // we don't want to steal it back.
+ if (
+ linkUIref.current.contains(
+ window.document.activeElement
+ )
+ ) {
+ // Select the previous block to keep focus nearby
+ selectPreviousBlock( clientId, true );
+ }
+
// Remove the link.
onReplace( [] );
return;
diff --git a/packages/block-library/src/navigation-link/link-ui.js b/packages/block-library/src/navigation-link/link-ui.js
index ce79af40e4708..6619c46253546 100644
--- a/packages/block-library/src/navigation-link/link-ui.js
+++ b/packages/block-library/src/navigation-link/link-ui.js
@@ -20,6 +20,7 @@ import {
useState,
useRef,
useEffect,
+ forwardRef,
} from '@wordpress/element';
import {
store as coreStore,
@@ -145,7 +146,7 @@ function LinkUIBlockInserter( { clientId, onBack, onSelectBlock } ) {
);
}
-export function LinkUI( props ) {
+function UnforwardedLinkUI( props, ref ) {
const [ addingBlock, setAddingBlock ] = useState( false );
const [ focusAddBlockButton, setFocusAddBlockButton ] = useState( false );
const { saveEntityRecord } = useDispatch( coreStore );
@@ -214,6 +215,7 @@ export function LinkUI( props ) {
return (
{
const blockInserterAriaRole = 'listbox';
const addBlockButtonRef = useRef();
diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php
index ba36acf139a62..940277d47a2af 100644
--- a/packages/block-library/src/navigation/index.php
+++ b/packages/block-library/src/navigation/index.php
@@ -1674,9 +1674,15 @@ function block_core_navigation_insert_hooked_blocks_into_rest_response( $respons
$rest_prepare_wp_navigation_core_callback = 'block_core_navigation_' . 'insert_hooked_blocks_into_rest_response';
/*
- * Injection of hooked blocks into the Navigation block relies on some functions present in WP >= 6.5
- * that are not present in Gutenberg's WP 6.5 compatibility layer.
+ * Do not add the `block_core_navigation_insert_hooked_blocks_into_rest_response` filter in the following cases:
+ * - If Core has added the `insert_hooked_blocks_into_rest_response` filter already (WP >= 6.6);
+ * - or if the `set_ignored_hooked_blocks_metadata` function is unavailable (which is required for the filter to work. It was introduced by WP 6.5 but is not present in Gutenberg's WP 6.5 compatibility layer);
+ * - or if the `$rest_prepare_wp_navigation_core_callback` filter has already been added.
*/
-if ( function_exists( 'set_ignored_hooked_blocks_metadata' ) && ! has_filter( 'rest_prepare_wp_navigation', $rest_prepare_wp_navigation_core_callback ) ) {
+if (
+ ! has_filter( 'rest_prepare_wp_navigation', 'insert_hooked_blocks_into_rest_response' ) &&
+ function_exists( 'set_ignored_hooked_blocks_metadata' ) &&
+ ! has_filter( 'rest_prepare_wp_navigation', $rest_prepare_wp_navigation_core_callback )
+) {
add_filter( 'rest_prepare_wp_navigation', 'block_core_navigation_insert_hooked_blocks_into_rest_response', 10, 3 );
}
diff --git a/packages/block-library/src/spacer/controls.js b/packages/block-library/src/spacer/controls.js
index 91a1e79be173e..160335fcc092e 100644
--- a/packages/block-library/src/spacer/controls.js
+++ b/packages/block-library/src/spacer/controls.js
@@ -7,6 +7,7 @@ import {
useSettings,
__experimentalSpacingSizesControl as SpacingSizesControl,
isValueSpacingPreset,
+ privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
import {
BaseControl,
@@ -21,14 +22,15 @@ import { View } from '@wordpress/primitives';
/**
* Internal dependencies
*/
+import { unlock } from '../lock-unlock';
import { MIN_SPACER_SIZE } from './constants';
+const { useSpacingSizes } = unlock( blockEditorPrivateApis );
+
function DimensionInput( { label, onChange, isResizing, value = '' } ) {
const inputId = useInstanceId( UnitControl, 'block-spacer-height-input' );
- const [ spacingSizes, spacingUnits ] = useSettings(
- 'spacing.spacingSizes',
- 'spacing.units'
- );
+ const spacingSizes = useSpacingSizes();
+ const [ spacingUnits ] = useSettings( 'spacing.units' );
// In most contexts the spacer size cannot meaningfully be set to a
// percentage, since this is relative to the parent container. This
// unit is disabled from the UI.
diff --git a/packages/block-library/src/spacer/edit.js b/packages/block-library/src/spacer/edit.js
index 03fc59e07ed17..1e0ffb9700d68 100644
--- a/packages/block-library/src/spacer/edit.js
+++ b/packages/block-library/src/spacer/edit.js
@@ -8,10 +8,10 @@ import clsx from 'clsx';
*/
import {
useBlockProps,
- useSettings,
getCustomValueFromPreset,
getSpacingPresetCssVar,
store as blockEditorStore,
+ privateApis as blockEditorPrivateApis,
} from '@wordpress/block-editor';
import { ResizableBox } from '@wordpress/components';
import { useState, useEffect } from '@wordpress/element';
@@ -21,9 +21,12 @@ import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
+import { unlock } from '../lock-unlock';
import SpacerControls from './controls';
import { MIN_SPACER_SIZE } from './constants';
+const { useSpacingSizes } = unlock( blockEditorPrivateApis );
+
const ResizableSpacer = ( {
orientation,
onResizeStart,
@@ -112,7 +115,7 @@ const SpacerEdit = ( {
const { layout = {} } = blockStyle;
const { selfStretch, flexSize } = layout;
- const [ spacingSizes ] = useSettings( 'spacing.spacingSizes' );
+ const spacingSizes = useSpacingSizes();
const [ isResizing, setIsResizing ] = useState( false );
const [ temporaryHeight, setTemporaryHeight ] = useState( null );
diff --git a/packages/blocks/src/api/parser/convert-legacy-block.js b/packages/blocks/src/api/parser/convert-legacy-block.js
index 8396b98109792..055679302efd6 100644
--- a/packages/blocks/src/api/parser/convert-legacy-block.js
+++ b/packages/blocks/src/api/parser/convert-legacy-block.js
@@ -88,25 +88,41 @@ export function convertLegacyBlockNameAndAttributes( name, attributes ) {
( name === 'core/paragraph' ||
name === 'core/heading' ||
name === 'core/image' ||
- name === 'core/button' )
+ name === 'core/button' ) &&
+ newAttributes.metadata.bindings.__default?.source !==
+ 'core/pattern-overrides'
) {
const bindings = [
'content',
'url',
'title',
+ 'id',
'alt',
'text',
'linkTarget',
];
+ // Delete any existing individual bindings and add a default binding.
+ // It was only possible to add all the default attributes through the UI,
+ // So as soon as we find an attribute, we can assume all default attributes are overridable.
+ let hasPatternOverrides = false;
bindings.forEach( ( binding ) => {
if (
- newAttributes.metadata.bindings[ binding ]?.source?.name ===
- 'pattern_attributes'
+ newAttributes.metadata.bindings[ binding ]?.source ===
+ 'core/pattern-overrides'
) {
- newAttributes.metadata.bindings[ binding ].source =
- 'core/pattern-overrides';
+ hasPatternOverrides = true;
+ newAttributes.metadata = {
+ ...newAttributes.metadata,
+ bindings: { ...newAttributes.metadata.bindings },
+ };
+ delete newAttributes.metadata.bindings[ binding ];
}
} );
+ if ( hasPatternOverrides ) {
+ newAttributes.metadata.bindings.__default = {
+ source: 'core/pattern-overrides',
+ };
+ }
}
}
return [ name, newAttributes ];
diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js
index a47d9aacab37a..dd6650338d9d1 100644
--- a/packages/blocks/src/store/private-actions.js
+++ b/packages/blocks/src/store/private-actions.js
@@ -55,6 +55,6 @@ export function registerBlockBindingsSource( source ) {
setValue: source.setValue,
setValues: source.setValues,
getPlaceholder: source.getPlaceholder,
- lockAttributesEditing: source.lockAttributesEditing,
+ canUserEditValue: source.canUserEditValue,
};
}
diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js
index 1d0d8cb2e968f..c00810c534d55 100644
--- a/packages/blocks/src/store/reducer.js
+++ b/packages/blocks/src/store/reducer.js
@@ -381,10 +381,7 @@ export function blockBindingsSources( state = {}, action ) {
setValue: action.setValue,
setValues: action.setValues,
getPlaceholder: action.getPlaceholder,
- lockAttributesEditing: () =>
- action.lockAttributesEditing
- ? action.lockAttributesEditing()
- : true,
+ canUserEditValue: action.canUserEditValue || ( () => false ),
},
};
}
diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js
index 9b2beb0b8369b..ecdc82e763304 100644
--- a/packages/blocks/src/store/selectors.js
+++ b/packages/blocks/src/store/selectors.js
@@ -237,10 +237,17 @@ export const getBlockVariations = createSelector(
export function getActiveBlockVariation( state, blockName, attributes, scope ) {
const variations = getBlockVariations( state, blockName, scope );
- const match = variations?.find( ( variation ) => {
+ if ( ! variations ) {
+ return variations;
+ }
+
+ const blockType = getBlockType( state, blockName );
+ const attributeKeys = Object.keys( blockType?.attributes || {} );
+ let match;
+ let maxMatchedAttributes = 0;
+
+ for ( const variation of variations ) {
if ( Array.isArray( variation.isActive ) ) {
- const blockType = getBlockType( state, blockName );
- const attributeKeys = Object.keys( blockType?.attributes || {} );
const definedAttributes = variation.isActive.filter(
( attribute ) => {
// We support nested attribute paths, e.g. `layout.type`.
@@ -250,10 +257,11 @@ export function getActiveBlockVariation( state, blockName, attributes, scope ) {
return attributeKeys.includes( topLevelAttribute );
}
);
- if ( definedAttributes.length === 0 ) {
- return false;
+ const definedAttributesLength = definedAttributes.length;
+ if ( definedAttributesLength === 0 ) {
+ continue;
}
- return definedAttributes.every( ( attribute ) => {
+ const isMatch = definedAttributes.every( ( attribute ) => {
const attributeValue = getValueFromObjectPath(
attributes,
attribute
@@ -266,11 +274,17 @@ export function getActiveBlockVariation( state, blockName, attributes, scope ) {
getValueFromObjectPath( variation.attributes, attribute )
);
} );
+ if ( isMatch && definedAttributesLength > maxMatchedAttributes ) {
+ match = variation;
+ maxMatchedAttributes = definedAttributesLength;
+ }
+ } else if ( variation.isActive?.( attributes, variation.attributes ) ) {
+ // If isActive is a function, we cannot know how many attributes it matches.
+ // This means that we cannot compare the specificity of our matches,
+ // and simply return the best match we have found.
+ return match || variation;
}
-
- return variation.isActive?.( attributes, variation.attributes );
- } );
-
+ }
return match;
}
diff --git a/packages/blocks/src/store/test/selectors.js b/packages/blocks/src/store/test/selectors.js
index 5e3a56c34cf3b..1a6e768724acc 100644
--- a/packages/blocks/src/store/test/selectors.js
+++ b/packages/blocks/src/store/test/selectors.js
@@ -291,6 +291,7 @@ describe( 'selectors', () => {
testAttribute: {},
firstTestAttribute: {},
secondTestAttribute: {},
+ thirdTestAttribute: {},
},
};
const FIRST_VARIATION_TEST_ATTRIBUTE_VALUE = 1;
@@ -510,6 +511,136 @@ describe( 'selectors', () => {
} )
).toEqual( variations[ 2 ] );
} );
+ it( 'should return the active variation using the match with the highest specificity for the given isActive array (multiple values)', () => {
+ const variations = [
+ {
+ name: 'variation-1',
+ attributes: {
+ firstTestAttribute: 1,
+ secondTestAttribute: 2,
+ },
+ isActive: [
+ 'firstTestAttribute',
+ 'secondTestAttribute',
+ ],
+ },
+ {
+ name: 'variation-2',
+ attributes: {
+ firstTestAttribute: 1,
+ secondTestAttribute: 2,
+ thirdTestAttribute: 3,
+ },
+ isActive: [
+ 'firstTestAttribute',
+ 'secondTestAttribute',
+ 'thirdTestAttribute',
+ ],
+ },
+ {
+ name: 'variation-3',
+ attributes: {
+ firstTestAttribute: 1,
+ thirdTestAttribute: 3,
+ },
+ isActive: [
+ 'firstTestAttribute',
+ 'thirdTestAttribute',
+ ],
+ },
+ ];
+
+ const state =
+ createBlockVariationsStateWithTestBlockType( variations );
+
+ expect(
+ getActiveBlockVariation( state, blockName, {
+ firstTestAttribute: 1,
+ secondTestAttribute: 2,
+ } )
+ ).toEqual( variations[ 0 ] );
+ // All variations match the following attributes. Since all matches have an array for their isActive
+ // fields, we can compare the specificity of each match and return the most specific match.
+ expect(
+ getActiveBlockVariation( state, blockName, {
+ firstTestAttribute: 1,
+ secondTestAttribute: 2,
+ thirdTestAttribute: 3,
+ } )
+ ).toEqual( variations[ 1 ] );
+ expect(
+ getActiveBlockVariation( state, blockName, {
+ firstTestAttribute: 1,
+ thirdTestAttribute: 3,
+ } )
+ ).toEqual( variations[ 2 ] );
+ } );
+ it( 'should return the active variation using the first match given the isActive array (multiple values) and function', () => {
+ const variations = [
+ {
+ name: 'variation-1',
+ attributes: {
+ firstTestAttribute: 1,
+ secondTestAttribute: 2,
+ },
+ isActive: [
+ 'firstTestAttribute',
+ 'secondTestAttribute',
+ ],
+ },
+ {
+ name: 'variation-2',
+ attributes: {
+ firstTestAttribute: 1,
+ secondTestAttribute: 2,
+ thirdTestAttribute: 3,
+ },
+ isActive: [
+ 'firstTestAttribute',
+ 'secondTestAttribute',
+ 'thirdTestAttribute',
+ ],
+ },
+ {
+ name: 'variation-3',
+ attributes: {
+ firstTestAttribute: 1,
+ thirdTestAttribute: 3,
+ },
+ isActive: ( blockAttributes, variationAttributes ) =>
+ blockAttributes.firstTestAttribute ===
+ variationAttributes.firstTestAttribute &&
+ blockAttributes.thirdTestAttribute ===
+ variationAttributes.thirdTestAttribute,
+ },
+ ];
+
+ const state =
+ createBlockVariationsStateWithTestBlockType( variations );
+
+ expect(
+ getActiveBlockVariation( state, blockName, {
+ firstTestAttribute: 1,
+ secondTestAttribute: 2,
+ } )
+ ).toEqual( variations[ 0 ] );
+ // All variations match the following attributes. However, since the third variation has a function
+ // for its isActive field, we cannot compare the specificity of each match, so instead we return the
+ // best match we've found.
+ expect(
+ getActiveBlockVariation( state, blockName, {
+ firstTestAttribute: 1,
+ secondTestAttribute: 2,
+ thirdTestAttribute: 3,
+ } )
+ ).toEqual( variations[ 1 ] );
+ expect(
+ getActiveBlockVariation( state, blockName, {
+ firstTestAttribute: 1,
+ thirdTestAttribute: 3,
+ } )
+ ).toEqual( variations[ 2 ] );
+ } );
it( 'should ignore attributes that are not defined in the block type', () => {
const variations = [
{
diff --git a/packages/components/src/custom-select-control/index.js b/packages/components/src/custom-select-control/index.js
index 979aa0f7bdff8..58ca5a961a138 100644
--- a/packages/components/src/custom-select-control/index.js
+++ b/packages/components/src/custom-select-control/index.js
@@ -114,7 +114,7 @@ export default function CustomSelectControl( props ) {
return sprintf( __( 'Currently selected: %s' ), selectedItem.name );
}
- const menuProps = getMenuProps( {
+ let menuProps = getMenuProps( {
className: 'components-custom-select-control__menu',
'aria-hidden': ! isOpen,
} );
@@ -131,7 +131,11 @@ export default function CustomSelectControl( props ) {
if (
menuProps[ 'aria-activedescendant' ]?.startsWith( 'downshift-null' )
) {
- delete menuProps[ 'aria-activedescendant' ];
+ const {
+ 'aria-activedescendant': ariaActivedescendant,
+ ...restMenuProps
+ } = menuProps;
+ menuProps = restMenuProps;
}
return (
+-
### createHigherOrderComponent
@@ -145,7 +145,7 @@ This is inspired by `lodash`'s `flow` function.
_Related_
--
+-
### pure
@@ -247,7 +247,7 @@ Debounces a function similar to Lodash's `debounce`. A new debounced function wi
_Related_
--
+-
_Parameters_
@@ -535,7 +535,7 @@ Throttles a function similar to Lodash's `throttle`. A new throttled function wi
_Related_
--
+-
_Parameters_
diff --git a/packages/compose/src/higher-order/compose.ts b/packages/compose/src/higher-order/compose.ts
index 2b7b598c70c95..1fcaa55eb4512 100644
--- a/packages/compose/src/higher-order/compose.ts
+++ b/packages/compose/src/higher-order/compose.ts
@@ -9,7 +9,7 @@ import { basePipe } from './pipe';
*
* This is inspired by `lodash`'s `flowRight` function.
*
- * @see https://docs-lodash.com/v4/flow-right/
+ * @see https://lodash.com/docs/4#flow-right
*/
const compose = basePipe( true );
diff --git a/packages/compose/src/higher-order/pipe.ts b/packages/compose/src/higher-order/pipe.ts
index ced7618ad81bf..29003d818ccc3 100644
--- a/packages/compose/src/higher-order/pipe.ts
+++ b/packages/compose/src/higher-order/pipe.ts
@@ -43,7 +43,7 @@
*
* Allows to choose whether to perform left-to-right or right-to-left composition.
*
- * @see https://docs-lodash.com/v4/flow/
+ * @see https://lodash.com/docs/4#flow
*
* @param {boolean} reverse True if right-to-left, false for left-to-right composition.
*/
@@ -67,7 +67,7 @@ const basePipe =
*
* This is inspired by `lodash`'s `flow` function.
*
- * @see https://docs-lodash.com/v4/flow/
+ * @see https://lodash.com/docs/4#flow
*/
const pipe = basePipe();
diff --git a/packages/compose/src/hooks/use-debounce/index.js b/packages/compose/src/hooks/use-debounce/index.js
index 5d48dba91bde6..6da42f159a864 100644
--- a/packages/compose/src/hooks/use-debounce/index.js
+++ b/packages/compose/src/hooks/use-debounce/index.js
@@ -19,7 +19,7 @@ import { debounce } from '../../utils/debounce';
* including the function to debounce, so please wrap functions created on
* render in components in `useCallback`.
*
- * @see https://docs-lodash.com/v4/debounce/
+ * @see https://lodash.com/docs/4#debounce
*
* @template {(...args: any[]) => void} TFunc
*
diff --git a/packages/compose/src/hooks/use-throttle/index.js b/packages/compose/src/hooks/use-throttle/index.js
index 8cade9a8442ac..4bf6cc6e85555 100644
--- a/packages/compose/src/hooks/use-throttle/index.js
+++ b/packages/compose/src/hooks/use-throttle/index.js
@@ -19,7 +19,7 @@ import { throttle } from '../../utils/throttle';
* including the function to throttle, so please wrap functions created on
* render in components in `useCallback`.
*
- * @see https://docs-lodash.com/v4/throttle/
+ * @see https://lodash.com/docs/4#throttle
*
* @template {(...args: any[]) => void} TFunc
*
diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js
index be4d12f0cb9ef..36b0ba5f84c9d 100644
--- a/packages/core-data/src/actions.js
+++ b/packages/core-data/src/actions.js
@@ -598,10 +598,14 @@ export const saveEntityRecord =
return acc;
},
{
+ // Do not update the `status` if we have edited it when auto saving.
+ // It's very important to let the user explicitly save this change,
+ // because it can lead to unexpected results. An example would be to
+ // have a draft post and change the status to publish.
status:
data.status === 'auto-draft'
? 'draft'
- : data.status,
+ : undefined,
}
);
updatedRecord = await __unstableFetch( {
diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js
index b79cefa096625..6603b1f685152 100644
--- a/packages/edit-post/src/components/layout/index.js
+++ b/packages/edit-post/src/components/layout/index.js
@@ -11,6 +11,7 @@ import {
LocalAutosaveMonitor,
UnsavedChangesWarning,
EditorKeyboardShortcutsRegister,
+ EditorSnackbars,
store as editorStore,
privateApis as editorPrivateApis,
} from '@wordpress/editor';
@@ -294,6 +295,7 @@ function Layout( { initialPost } ) {
) }
+
"
`;
+
+exports[`Editor on content update parses markdown into blocks 1`] = `
+"
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
+
+
+Overview
+
+
+
+
+Lorem ipsum dolor sit amet
+
+
+
+Consectetur adipiscing elit
+
+
+
+Integer nec odio
+
+
+
+
+Details
+
+
+
+
+Sed cursus ante dapibus diam
+
+
+
+Nulla quis sem at nibh elementum imperdiet
+
+
+
+Duis sagittis ipsum ## Mixed Lists
+
+
+
+
+
+
+
+
+
+Lorem ipsum dolor sit amet
+
+
+
+ Consectetur adipiscing elit
+
+
+
+Integer nec odio
+
+
+
+
+
+Additional Info: - Sed cursus ante dapibus diam
+
+
+
+Nulla quis sem at nibh elementum imperdiet
+
+"
+`;
diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js
index 0de2c528b2452..acafc4d68d42a 100644
--- a/packages/edit-post/src/test/editor.native.js
+++ b/packages/edit-post/src/test/editor.native.js
@@ -7,6 +7,7 @@ import {
fireEvent,
getBlock,
getEditorHtml,
+ getEditorTitle,
initializeEditor,
screen,
setupCoreBlocks,
@@ -20,6 +21,7 @@ import {
requestMediaImport,
subscribeMediaAppend,
subscribeParentToggleHTMLMode,
+ subscribeToContentUpdate,
} from '@wordpress/react-native-bridge';
setupCoreBlocks();
@@ -34,6 +36,11 @@ subscribeMediaAppend.mockImplementation( ( callback ) => {
mediaAppendCallback = callback;
} );
+let onContentUpdateCallback;
+subscribeToContentUpdate.mockImplementation( ( callback ) => {
+ onContentUpdateCallback = callback;
+} );
+
const MEDIA = [
{
localId: 1,
@@ -149,4 +156,74 @@ describe( 'Editor', () => {
screen.queryAllByLabelText( 'Open Settings' );
expect( openBlockSettingsButton.length ).toBe( 0 );
} );
+
+ describe( 'on content update', () => {
+ const MARKDOWN = `# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing
+ elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing
+ elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2.
+ Nulla quis sem at nibh elementum imperdiet\n3. Duis sagittis ipsum\n
+ ## Mixed Lists\n- Key Points:\n 1. Lorem ipsum dolor sit amet\n 2.
+ Consectetur adipiscing elit\n 3. Integer nec odio\n- Additional Info:\n -
+ Sed cursus ante dapibus diam\n - Nulla quis sem at nibh elementum imperdiet\n`;
+
+ it( 'parses markdown into blocks', async () => {
+ // Arrange
+ await initializeEditor( {
+ initialTitle: null,
+ } );
+
+ // Act
+ act( () => {
+ onContentUpdateCallback( {
+ content: MARKDOWN,
+ } );
+ } );
+
+ // Assert
+ // Needed to for the "Processed HTML piece" log.
+ expect( console ).toHaveLogged();
+ expect( getEditorTitle() ).toBe( 'Sample Document' );
+ expect( getEditorHtml() ).toMatchSnapshot();
+ } );
+
+ it( 'parses a markdown heading into a title', async () => {
+ // Arrange
+ await initializeEditor( {
+ initialTitle: null,
+ } );
+
+ // Act
+ act( () => {
+ onContentUpdateCallback( {
+ content: `# Sample Document`,
+ } );
+ } );
+
+ // Assert
+ // Needed to for the "Processed HTML piece" log.
+ expect( console ).toHaveLogged();
+ expect( getEditorTitle() ).toBe( 'Sample Document' );
+ expect( getEditorHtml() ).toBe( '' );
+ } );
+
+ it( 'parses standard text into blocks', async () => {
+ // Arrange
+ await initializeEditor( {
+ initialTitle: null,
+ } );
+
+ // Act
+ act( () => {
+ onContentUpdateCallback( {
+ content: `Lorem ipsum dolor sit amet`,
+ } );
+ } );
+
+ // Assert
+ // Needed to for the "Processed HTML piece" log.
+ expect( console ).toHaveLogged();
+ expect( getEditorTitle() ).toBe( 'Lorem ipsum dolor sit amet' );
+ expect( getEditorHtml() ).toBe( '' );
+ } );
+ } );
} );
diff --git a/packages/edit-site/src/components/add-new-template/index.js b/packages/edit-site/src/components/add-new-template/index.js
index 75d69f32e823d..3705010be4853 100644
--- a/packages/edit-site/src/components/add-new-template/index.js
+++ b/packages/edit-site/src/components/add-new-template/index.js
@@ -128,6 +128,7 @@ function TemplateListItem( {
spacing={ 0 }
>
diff --git a/packages/edit-site/src/components/block-editor/site-editor-canvas.js b/packages/edit-site/src/components/block-editor/site-editor-canvas.js
deleted file mode 100644
index 03d11dcf8237e..0000000000000
--- a/packages/edit-site/src/components/block-editor/site-editor-canvas.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Internal dependencies
- */
-import EditorCanvas from './editor-canvas';
-import EditorCanvasContainer from '../editor-canvas-container';
-import useSiteEditorSettings from './use-site-editor-settings';
-
-export default function SiteEditorCanvas() {
- const settings = useSiteEditorSettings();
-
- return (
-
- { ( [ editorCanvasView ] ) =>
- editorCanvasView ? (
-
- { editorCanvasView }
-
- ) : (
-
- )
- }
-
- );
-}
diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss
index 14f2074130d11..3849eed24a4ea 100644
--- a/packages/edit-site/src/components/block-editor/style.scss
+++ b/packages/edit-site/src/components/block-editor/style.scss
@@ -1,29 +1,3 @@
-// The button element easily inherits styles that are meant for the editor style.
-// These rules enhance the specificity to reduce that inheritance.
-// This is duplicated in visual-editor.
-.edit-site-block-editor__editor-styles-wrapper .components-button {
- font-family: $default-font;
- font-size: $default-font-size;
- padding: 6px 12px;
-
- &.is-tertiary,
- &.has-icon {
- padding: 6px;
- }
-}
-
-.edit-site-visual-editor {
- height: 100%;
- background-color: $gray-300;
-
- // Controls height of editor and editor canvas container (style book, global styles revisions previews etc.)
- iframe {
- display: block;
- width: 100%;
- height: 100%;
- }
-}
-
.edit-site-visual-editor__editor-canvas {
&.is-focused {
outline: calc(2 * var(--wp-admin-border-width-focus)) solid var(--wp-admin-theme-color);
diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/use-editor-iframe-props.js
similarity index 62%
rename from packages/edit-site/src/components/block-editor/editor-canvas.js
rename to packages/edit-site/src/components/block-editor/use-editor-iframe-props.js
index bac51c5df7d33..3b5c9f7fe6572 100644
--- a/packages/edit-site/src/components/block-editor/editor-canvas.js
+++ b/packages/edit-site/src/components/block-editor/use-editor-iframe-props.js
@@ -8,12 +8,9 @@ import clsx from 'clsx';
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { ENTER, SPACE } from '@wordpress/keycodes';
-import { useState, useEffect, useMemo } from '@wordpress/element';
+import { useState, useEffect } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
-import {
- store as editorStore,
- privateApis as editorPrivateApis,
-} from '@wordpress/editor';
+import { store as editorStore } from '@wordpress/editor';
/**
* Internal dependencies
@@ -21,9 +18,7 @@ import {
import { unlock } from '../../lock-unlock';
import { store as editSiteStore } from '../../store';
-const { VisualEditor } = unlock( editorPrivateApis );
-
-function EditorCanvas( { settings } ) {
+export default function useEditorIframeProps() {
const { canvasMode, currentPostIsTrashed } = useSelect( ( select ) => {
const { getCanvasMode } = unlock( select( editSiteStore ) );
@@ -75,36 +70,10 @@ function EditorCanvas( { settings } ) {
readonly: true,
};
- const styles = useMemo(
- () => [
- ...settings.styles,
- {
- // Forming a "block formatting context" to prevent margin collapsing.
- // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context
-
- css: `body{${
- canvasMode === 'view'
- ? `min-height: 100vh; ${
- currentPostIsTrashed ? '' : 'cursor: pointer;'
- }`
- : ''
- }}}`,
- },
- ],
- [ settings.styles, canvasMode, currentPostIsTrashed ]
- );
-
- return (
-
- );
+ return {
+ className: clsx( 'edit-site-visual-editor__editor-canvas', {
+ 'is-focused': isFocused && canvasMode === 'view',
+ } ),
+ ...( canvasMode === 'view' ? viewModeIframeProps : {} ),
+ };
}
-
-export default EditorCanvas;
diff --git a/packages/edit-site/src/components/editor-canvas-container/index.js b/packages/edit-site/src/components/editor-canvas-container/index.js
index ca5bc3a0b34f4..baebc1bc1bc89 100644
--- a/packages/edit-site/src/components/editor-canvas-container/index.js
+++ b/packages/edit-site/src/components/editor-canvas-container/index.js
@@ -4,7 +4,6 @@
import { Children, cloneElement, useState } from '@wordpress/element';
import {
Button,
- privateApis as componentsPrivateApis,
__experimentalUseSlotFills as useSlotFills,
} from '@wordpress/components';
import { ESCAPE } from '@wordpress/keycodes';
@@ -24,7 +23,7 @@ import {
import { unlock } from '../../lock-unlock';
import { store as editSiteStore } from '../../store';
-const { ResizableEditor } = unlock( editorPrivateApis );
+const { EditorContentSlotFill, ResizableEditor } = unlock( editorPrivateApis );
/**
* Returns a translated string for the title of the editor canvas container.
@@ -45,15 +44,6 @@ function getEditorCanvasContainerTitle( view ) {
}
}
-// Creates a private slot fill.
-const { createPrivateSlotFill } = unlock( componentsPrivateApis );
-const SLOT_FILL_NAME = 'EditSiteEditorCanvasContainerSlot';
-const {
- privateKey,
- Slot: EditorCanvasContainerSlot,
- Fill: EditorCanvasContainerFill,
-} = createPrivateSlotFill( SLOT_FILL_NAME );
-
function EditorCanvasContainer( {
children,
closeButtonLabel,
@@ -123,7 +113,7 @@ function EditorCanvasContainer( {
const shouldShowCloseButton = onClose || closeButtonLabel;
return (
-
+
{ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ }
@@ -145,14 +135,13 @@ function EditorCanvasContainer( {
-
+
);
}
function useHasEditorCanvasContainer() {
- const fills = useSlotFills( privateKey );
+ const fills = useSlotFills( EditorContentSlotFill.privateKey );
return !! fills?.length;
}
-EditorCanvasContainer.Slot = EditorCanvasContainerSlot;
export default EditorCanvasContainer;
export { useHasEditorCanvasContainer, getEditorCanvasContainerTitle };
diff --git a/packages/edit-site/src/components/editor-canvas-container/style.scss b/packages/edit-site/src/components/editor-canvas-container/style.scss
index 0aca5f8045ce8..fad566212e732 100644
--- a/packages/edit-site/src/components/editor-canvas-container/style.scss
+++ b/packages/edit-site/src/components/editor-canvas-container/style.scss
@@ -1,5 +1,13 @@
.edit-site-editor-canvas-container {
height: 100%;
+ background-color: $gray-300;
+
+ // Controls height of editor and editor canvas container (style book, global styles revisions previews etc.)
+ iframe {
+ display: block;
+ width: 100%;
+ height: 100%;
+ }
.edit-site-layout.is-full-canvas & {
padding: $grid-unit-30 $grid-unit-30 0;
diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js
index ada8367f70174..26b20b66f11c7 100644
--- a/packages/edit-site/src/components/editor/index.js
+++ b/packages/edit-site/src/components/editor/index.js
@@ -7,39 +7,24 @@ import clsx from 'clsx';
* WordPress dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
-import {
- Notice,
- __unstableAnimatePresence as AnimatePresence,
- __unstableMotion as motion,
-} from '@wordpress/components';
-import {
- useInstanceId,
- useViewportMatch,
- useReducedMotion,
-} from '@wordpress/compose';
-import { store as preferencesStore } from '@wordpress/preferences';
-import {
- BlockBreadcrumb,
- BlockToolbar,
- store as blockEditorStore,
-} from '@wordpress/block-editor';
+import { Notice } from '@wordpress/components';
+import { useInstanceId } from '@wordpress/compose';
import {
EditorKeyboardShortcutsRegister,
- EditorNotices,
privateApis as editorPrivateApis,
store as editorStore,
} from '@wordpress/editor';
import { __, sprintf } from '@wordpress/i18n';
import { store as coreDataStore } from '@wordpress/core-data';
import { privateApis as blockLibraryPrivateApis } from '@wordpress/block-library';
-import { useState, useCallback } from '@wordpress/element';
+import { useCallback, useMemo } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { privateApis as routerPrivateApis } from '@wordpress/router';
+import { store as preferencesStore } from '@wordpress/preferences';
/**
* Internal dependencies
*/
-import Header from '../header-edit-mode';
import WelcomeGuide from '../welcome-guide';
import { store as editSiteStore } from '../../store';
import { GlobalStylesRenderer } from '../global-styles-renderer';
@@ -48,79 +33,51 @@ import CanvasLoader from '../canvas-loader';
import { unlock } from '../../lock-unlock';
import useEditedEntityRecord from '../use-edited-entity-record';
import { POST_TYPE_LABELS, TEMPLATE_POST_TYPE } from '../../utils/constants';
-import SiteEditorCanvas from '../block-editor/site-editor-canvas';
import TemplatePartConverter from '../template-part-converter';
import { useSpecificEditorSettings } from '../block-editor/use-site-editor-settings';
import PluginTemplateSettingPanel from '../plugin-template-setting-panel';
import GlobalStylesSidebar from '../global-styles-sidebar';
+import { isPreviewingTheme } from '../../utils/is-previewing-theme';
+import {
+ getEditorCanvasContainerTitle,
+ useHasEditorCanvasContainer,
+} from '../editor-canvas-container';
+import SaveButton from '../save-button';
+import SiteEditorMoreMenu from '../more-menu';
+import useEditorIframeProps from '../block-editor/use-editor-iframe-props';
const {
+ EditorInterface,
ExperimentalEditorProvider: EditorProvider,
- InserterSidebar,
- ListViewSidebar,
- InterfaceSkeleton,
- ComplementaryArea,
- SavePublishPanels,
Sidebar,
- TextEditor,
} = unlock( editorPrivateApis );
const { useHistory } = unlock( routerPrivateApis );
const { BlockKeyboardShortcuts } = unlock( blockLibraryPrivateApis );
-const interfaceLabels = {
- /* translators: accessibility text for the editor content landmark region. */
- body: __( 'Editor content' ),
- /* translators: accessibility text for the editor settings landmark region. */
- sidebar: __( 'Editor settings' ),
- /* translators: accessibility text for the editor publish landmark region. */
- actions: __( 'Editor publish' ),
- /* translators: accessibility text for the editor footer landmark region. */
- footer: __( 'Editor footer' ),
- /* translators: accessibility text for the editor header landmark region. */
- header: __( 'Editor top bar' ),
-};
-
-const ANIMATION_DURATION = 0.25;
-
export default function Editor( { isLoading } ) {
const {
record: editedPost,
getTitle,
isLoaded: hasLoadedPost,
} = useEditedEntityRecord();
-
const { type: editedPostType } = editedPost;
-
- const isLargeViewport = useViewportMatch( 'medium' );
- const disableMotion = useReducedMotion();
-
const {
context,
contextPost,
editorMode,
canvasMode,
- blockEditorMode,
- isInserterOpen,
- isListViewOpen,
- isDistractionFree,
- showIconLabels,
- showBlockBreadcrumbs,
- postTypeLabel,
isEditingPage,
supportsGlobalStyles,
+ showIconLabels,
+ editorCanvasView,
+ currentPostIsTrashed,
} = useSelect( ( select ) => {
- const { get } = select( preferencesStore );
const { getEditedPostContext, getCanvasMode, isPage } = unlock(
select( editSiteStore )
);
- const { __unstableGetEditorMode } = select( blockEditorStore );
+ const { get } = select( preferencesStore );
const { getEntityRecord, getCurrentTheme } = select( coreDataStore );
- const {
- isInserterOpened,
- isListViewOpened,
- getPostTypeLabel,
- getEditorMode,
- } = select( editorStore );
+ const { getEditorMode } = select( editorStore );
const _context = getEditedPostContext();
// The currently selected entity to display.
@@ -136,32 +93,24 @@ export default function Editor( { isLoading } ) {
: undefined,
editorMode: getEditorMode(),
canvasMode: getCanvasMode(),
- blockEditorMode: __unstableGetEditorMode(),
- isInserterOpen: isInserterOpened(),
- isListViewOpen: isListViewOpened(),
- isDistractionFree: get( 'core', 'distractionFree' ),
- showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ),
- showIconLabels: get( 'core', 'showIconLabels' ),
- postTypeLabel: getPostTypeLabel(),
isEditingPage: isPage(),
supportsGlobalStyles: getCurrentTheme()?.is_block_theme,
+ showIconLabels: get( 'core', 'showIconLabels' ),
+ editorCanvasView: unlock(
+ select( editSiteStore )
+ ).getEditorCanvasContainerView(),
+ currentPostIsTrashed:
+ select( editorStore ).getCurrentPostAttribute( 'status' ) ===
+ 'trash',
};
}, [] );
+ const _isPreviewingTheme = isPreviewingTheme();
+ const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer();
+ const iframeProps = useEditorIframeProps();
const isViewMode = canvasMode === 'view';
const isEditMode = canvasMode === 'edit';
const showVisualEditor = isViewMode || editorMode === 'visual';
- const shouldShowBlockBreadcrumbs =
- ! isDistractionFree &&
- showBlockBreadcrumbs &&
- isEditMode &&
- showVisualEditor &&
- blockEditorMode !== 'zoom-out';
- const shouldShowInserter = isEditMode && showVisualEditor && isInserterOpen;
- const shouldShowListView = isEditMode && showVisualEditor && isListViewOpen;
- const secondarySidebarLabel = isListViewOpen
- ? __( 'List View' )
- : __( 'Block Library' );
const postWithTemplate = !! context?.postId;
let title;
@@ -185,22 +134,24 @@ export default function Editor( { isLoading } ) {
);
const settings = useSpecificEditorSettings();
+ const styles = useMemo(
+ () => [
+ ...settings.styles,
+ {
+ // Forming a "block formatting context" to prevent margin collapsing.
+ // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context
- // Local state for save panel.
- // Note 'truthy' callback implies an open panel.
- const [ entitiesSavedStatesCallback, setEntitiesSavedStatesCallback ] =
- useState( false );
-
- const closeEntitiesSavedStates = useCallback(
- ( arg ) => {
- if ( typeof entitiesSavedStatesCallback === 'function' ) {
- entitiesSavedStatesCallback( arg );
- }
- setEntitiesSavedStatesCallback( false );
- },
- [ entitiesSavedStatesCallback ]
+ css: `body{${
+ canvasMode === 'view'
+ ? `min-height: 100vh; ${
+ currentPostIsTrashed ? '' : 'cursor: pointer;'
+ }`
+ : ''
+ }}}`,
+ },
+ ],
+ [ settings.styles, canvasMode, currentPostIsTrashed ]
);
-
const { createSuccessNotice } = useDispatch( noticesStore );
const history = useHistory();
const onActionPerformed = useCallback(
@@ -280,97 +231,28 @@ export default function Editor( { isLoading } ) {
settings={ settings }
useSubRegistry={ false }
>
-
+
- { canvasMode === 'edit' && (
-
-
-
- ) }
-
- }
- actions={
-
- }
- content={
- <>
- { isEditMode && }
- { editorMode === 'text' && isEditMode && (
-
- ) }
- { ! isLargeViewport && showVisualEditor && (
-
- ) }
- { showVisualEditor && }
- >
- }
- secondarySidebar={
- isEditMode &&
- ( ( shouldShowInserter && ) ||
- ( shouldShowListView && ) )
- }
- sidebar={
- isEditMode &&
- ! isDistractionFree && (
-
- )
+ styles={ styles }
+ enableRegionNavigation={ false }
+ customSaveButton={
+ _isPreviewingTheme &&
}
- footer={
- shouldShowBlockBreadcrumbs && (
-
- )
+ forceDisableBlockTools={ ! hasDefaultEditorCanvasView }
+ title={
+ ! hasDefaultEditorCanvasView
+ ? getEditorCanvasContainerTitle(
+ editorCanvasView
+ )
+ : undefined
}
- labels={ {
- ...interfaceLabels,
- secondarySidebar: secondarySidebarLabel,
- } }
+ iframeProps={ iframeProps }
/>
-
+
);
}
diff --git a/packages/edit-site/src/components/global-styles/font-families.js b/packages/edit-site/src/components/global-styles/font-families.js
index 9f381d9d86f18..7713b27e306ef 100644
--- a/packages/edit-site/src/components/global-styles/font-families.js
+++ b/packages/edit-site/src/components/global-styles/font-families.js
@@ -8,6 +8,7 @@ import {
__experimentalHStack as HStack,
Button,
} from '@wordpress/components';
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { settings } from '@wordpress/icons';
import { useContext } from '@wordpress/element';
@@ -20,18 +21,31 @@ import FontLibraryProvider, {
import FontLibraryModal from './font-library-modal';
import FontFamilyItem from './font-family-item';
import Subtitle from './subtitle';
+import { setUIValuesNeeded } from './font-library-modal/utils';
+import { unlock } from '../../lock-unlock';
-function FontFamilies() {
- const { modalTabOpen, toggleModal, themeFonts, customFonts } =
- useContext( FontLibraryContext );
+const { useGlobalSetting } = unlock( blockEditorPrivateApis );
+function FontFamilies() {
+ const { modalTabOpen, setModalTabOpen } = useContext( FontLibraryContext );
+ const [ fontFamilies ] = useGlobalSetting( 'typography.fontFamilies' );
+ const themeFonts = fontFamilies?.theme
+ ? fontFamilies.theme
+ .map( ( f ) => setUIValuesNeeded( f, { source: 'theme' } ) )
+ .sort( ( a, b ) => a.name.localeCompare( b.name ) )
+ : [];
+ const customFonts = fontFamilies?.custom
+ ? fontFamilies.custom
+ .map( ( f ) => setUIValuesNeeded( f, { source: 'custom' } ) )
+ .sort( ( a, b ) => a.name.localeCompare( b.name ) )
+ : [];
const hasFonts = 0 < customFonts.length || 0 < themeFonts.length;
return (
<>
{ !! modalTabOpen && (
toggleModal() }
+ onRequestClose={ () => setModalTabOpen( null ) }
defaultTabId={ modalTabOpen }
/>
) }
@@ -40,7 +54,7 @@ function FontFamilies() {
{ __( 'Fonts' ) }
toggleModal( 'installed-fonts' ) }
+ onClick={ () => setModalTabOpen( 'installed-fonts' ) }
label={ __( 'Manage fonts' ) }
icon={ settings }
size="small"
@@ -61,7 +75,7 @@ function FontFamilies() {
toggleModal( 'upload-fonts' ) }
+ onClick={ () => setModalTabOpen( 'upload-fonts' ) }
>
{ __( 'Add fonts' ) }
diff --git a/packages/edit-site/src/components/global-styles/font-family-item.js b/packages/edit-site/src/components/global-styles/font-family-item.js
index fc5418d5c7eb1..b80865fd2e21a 100644
--- a/packages/edit-site/src/components/global-styles/font-family-item.js
+++ b/packages/edit-site/src/components/global-styles/font-family-item.js
@@ -16,14 +16,14 @@ import { FontLibraryContext } from './font-library-modal/context';
import { getFamilyPreviewStyle } from './font-library-modal/utils/preview-styles';
function FontFamilyItem( { font } ) {
- const { handleSetLibraryFontSelected, toggleModal } =
+ const { handleSetLibraryFontSelected, setModalTabOpen } =
useContext( FontLibraryContext );
const variantsCount = font?.fontFace?.length || 1;
const handleClick = () => {
handleSetLibraryFontSelected( font );
- toggleModal( 'installed-fonts' );
+ setModalTabOpen( 'installed-fonts' );
};
const previewStyle = getFamilyPreviewStyle( font );
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js
index 9eafabe9424fb..52a40d7dc9070 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js
@@ -51,8 +51,6 @@ function FontLibraryProvider( { children } ) {
'globalStyles',
globalStylesId
);
- const fontFamiliesHasChanges =
- !! globalStyles?.edits?.settings?.typography?.fontFamilies;
const [ isInstalling, setIsInstalling ] = useState( false );
const [ refreshKey, setRefreshKey ] = useState( 0 );
@@ -62,14 +60,11 @@ function FontLibraryProvider( { children } ) {
setRefreshKey( Date.now() );
};
- const {
- records: libraryPosts = [],
- isResolving: isResolvingLibrary,
- hasResolved: hasResolvedLibrary,
- } = useEntityRecords( 'postType', 'wp_font_family', {
- refreshKey,
- _embed: true,
- } );
+ const { records: libraryPosts = [], isResolving: isResolvingLibrary } =
+ useEntityRecords( 'postType', 'wp_font_family', {
+ refreshKey,
+ _embed: true,
+ } );
const libraryFonts =
( libraryPosts || [] ).map( ( fontFamilyPost ) => {
@@ -87,12 +82,6 @@ function FontLibraryProvider( { children } ) {
const [ fontFamilies, setFontFamilies ] = useGlobalSetting(
'typography.fontFamilies'
);
- // theme.json file font families
- const [ baseFontFamilies ] = useGlobalSetting(
- 'typography.fontFamilies',
- undefined,
- 'base'
- );
/*
* Save the font families to the database.
@@ -131,24 +120,6 @@ function FontLibraryProvider( { children } ) {
.sort( ( a, b ) => a.name.localeCompare( b.name ) )
: [];
- const themeFontsSlugs = new Set( themeFonts.map( ( f ) => f.slug ) );
-
- /*
- * Base Theme Fonts are the fonts defined in the theme.json *file*.
- *
- * Uses the fonts from global styles + the ones from the theme.json file that hasn't repeated slugs.
- * Avoids inconsistencies with the fonts listed in the font library modal as base (inactivated).
- * These inconsistencies can happen when the active theme fonts in global styles aren't defined in theme.json file as when a theme style variation is applied.
- */
- const baseThemeFonts = baseFontFamilies?.theme
- ? themeFonts.concat(
- baseFontFamilies.theme
- .filter( ( f ) => ! themeFontsSlugs.has( f.slug ) )
- .map( ( f ) => setUIValuesNeeded( f, { source: 'theme' } ) )
- .sort( ( a, b ) => a.name.localeCompare( b.name ) )
- )
- : [];
-
const customFonts = fontFamilies?.custom
? fontFamilies.custom
.map( ( f ) => setUIValuesNeeded( f, { source: 'custom' } ) )
@@ -187,10 +158,6 @@ function FontLibraryProvider( { children } ) {
} );
};
- const toggleModal = ( tabName ) => {
- setModalTabOpen( tabName || null );
- };
-
// Demo
const [ loadedFontUrls ] = useState( new Set() );
@@ -549,9 +516,6 @@ function FontLibraryProvider( { children } ) {
libraryFontSelected,
handleSetLibraryFontSelected,
fontFamilies,
- themeFonts,
- baseThemeFonts,
- customFonts,
baseCustomFonts,
isFontActivated,
getFontFacesActivated,
@@ -561,14 +525,12 @@ function FontLibraryProvider( { children } ) {
toggleActivateFont,
getAvailableFontsOutline,
modalTabOpen,
- toggleModal,
+ setModalTabOpen,
refreshLibrary,
notice,
setNotice,
saveFontFamilies,
- fontFamiliesHasChanges,
isResolvingLibrary,
- hasResolvedLibrary,
isInstalling,
collections,
getFontCollection,
diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
index e817a49b4172d..fd962618b510a 100644
--- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
+++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js
@@ -17,11 +17,12 @@ import {
Notice,
ProgressBar,
} from '@wordpress/components';
-import { store as coreStore } from '@wordpress/core-data';
+import { useEntityRecord, store as coreStore } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { useContext, useEffect, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { chevronLeft } from '@wordpress/icons';
+import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
/**
* Internal dependencies
@@ -30,12 +31,15 @@ import { FontLibraryContext } from './context';
import FontCard from './font-card';
import LibraryFontVariant from './library-font-variant';
import { sortFontFaces } from './utils/sort-font-faces';
+import { setUIValuesNeeded } from './utils';
+import { unlock } from '../../../lock-unlock';
+
+const { useGlobalSetting } = unlock( blockEditorPrivateApis );
function InstalledFonts() {
const {
baseCustomFonts,
libraryFontSelected,
- baseThemeFonts,
handleSetLibraryFontSelected,
refreshLibrary,
uninstallFontFamily,
@@ -43,12 +47,44 @@ function InstalledFonts() {
isInstalling,
saveFontFamilies,
getFontFacesActivated,
- fontFamiliesHasChanges,
notice,
setNotice,
fontFamilies,
} = useContext( FontLibraryContext );
const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState( false );
+ const [ baseFontFamilies ] = useGlobalSetting(
+ 'typography.fontFamilies',
+ undefined,
+ 'base'
+ );
+ const globalStylesId = useSelect( ( select ) => {
+ const { __experimentalGetCurrentGlobalStylesId } = select( coreStore );
+ return __experimentalGetCurrentGlobalStylesId();
+ } );
+
+ const globalStyles = useEntityRecord(
+ 'root',
+ 'globalStyles',
+ globalStylesId
+ );
+ const fontFamiliesHasChanges =
+ !! globalStyles?.edits?.settings?.typography?.fontFamilies;
+
+ const themeFonts = fontFamilies?.theme
+ ? fontFamilies.theme
+ .map( ( f ) => setUIValuesNeeded( f, { source: 'theme' } ) )
+ .sort( ( a, b ) => a.name.localeCompare( b.name ) )
+ : [];
+ const themeFontsSlugs = new Set( themeFonts.map( ( f ) => f.slug ) );
+ const baseThemeFonts = baseFontFamilies?.theme
+ ? themeFonts.concat(
+ baseFontFamilies.theme
+ .filter( ( f ) => ! themeFontsSlugs.has( f.slug ) )
+ .map( ( f ) => setUIValuesNeeded( f, { source: 'theme' } ) )
+ .sort( ( a, b ) => a.name.localeCompare( b.name ) )
+ )
+ : [];
+
const customFontFamilyId =
libraryFontSelected?.source === 'custom' && libraryFontSelected?.id;
diff --git a/packages/edit-site/src/components/global-styles/shadows-edit-panel.js b/packages/edit-site/src/components/global-styles/shadows-edit-panel.js
index daa4869e10897..443d72deb9573 100644
--- a/packages/edit-site/src/components/global-styles/shadows-edit-panel.js
+++ b/packages/edit-site/src/components/global-styles/shadows-edit-panel.js
@@ -305,7 +305,7 @@ function ShadowEditor( { shadow, onChange } ) {
align="center"
className="edit-site-global-styles__shadows-panel__title"
>
- Shadows
+ { __( 'Shadows' ) }
{
- return {
- editorCanvasView: unlock(
- select( editSiteStore )
- ).getEditorCanvasContainerView(),
- };
- }, [] );
-
- return (
- <>
-
- }
- forceDisableBlockTools={ ! hasDefaultEditorCanvasView }
- title={
- ! hasDefaultEditorCanvasView
- ? getEditorCanvasContainerTitle( editorCanvasView )
- : undefined
- }
- />
-
- >
- );
-}
-
-export default Header;
diff --git a/packages/edit-site/src/components/header-edit-mode/style.scss b/packages/edit-site/src/components/header-edit-mode/style.scss
deleted file mode 100644
index 69b1e9dff3849..0000000000000
--- a/packages/edit-site/src/components/header-edit-mode/style.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.editor-header {
- padding-left: $header-height;
-}
diff --git a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js b/packages/edit-site/src/components/more-menu/index.js
similarity index 94%
rename from packages/edit-site/src/components/header-edit-mode/more-menu/index.js
rename to packages/edit-site/src/components/more-menu/index.js
index f0df5cf57fe6f..4c4e9bb45c093 100644
--- a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js
+++ b/packages/edit-site/src/components/more-menu/index.js
@@ -10,7 +10,7 @@ import { privateApis as editorPrivateApis } from '@wordpress/editor';
*/
import SiteExport from './site-export';
import WelcomeGuideMenuItem from './welcome-guide-menu-item';
-import { unlock } from '../../../lock-unlock';
+import { unlock } from '../../lock-unlock';
const { ToolsMoreMenuGroup, PreferencesModal } = unlock( editorPrivateApis );
diff --git a/packages/edit-site/src/components/header-edit-mode/more-menu/site-export.js b/packages/edit-site/src/components/more-menu/site-export.js
similarity index 100%
rename from packages/edit-site/src/components/header-edit-mode/more-menu/site-export.js
rename to packages/edit-site/src/components/more-menu/site-export.js
diff --git a/packages/edit-site/src/components/header-edit-mode/more-menu/welcome-guide-menu-item.js b/packages/edit-site/src/components/more-menu/welcome-guide-menu-item.js
similarity index 100%
rename from packages/edit-site/src/components/header-edit-mode/more-menu/welcome-guide-menu-item.js
rename to packages/edit-site/src/components/more-menu/welcome-guide-menu-item.js
diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js
index 6a10e0cd95d7b..e75faa60b92e9 100644
--- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js
+++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { trash, pages, drafts } from '@wordpress/icons';
+import { trash, pages, drafts, unseen, inbox } from '@wordpress/icons';
/**
* Internal dependencies
@@ -99,6 +99,36 @@ export const DEFAULT_VIEWS = {
],
},
},
+ {
+ title: __( 'Pending' ),
+ slug: 'pending',
+ icon: inbox,
+ view: {
+ ...DEFAULT_PAGE_BASE,
+ filters: [
+ {
+ field: 'status',
+ operator: OPERATOR_IS_ANY,
+ value: 'pending',
+ },
+ ],
+ },
+ },
+ {
+ title: __( 'Private' ),
+ slug: 'private',
+ icon: unseen,
+ view: {
+ ...DEFAULT_PAGE_BASE,
+ filters: [
+ {
+ field: 'status',
+ operator: OPERATOR_IS_ANY,
+ value: 'private',
+ },
+ ],
+ },
+ },
{
title: __( 'Trash' ),
slug: 'trash',
diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
index 31e359acb4027..29ce24cfb3b7b 100644
--- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
+++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js
@@ -96,7 +96,7 @@ function SidebarNavigationScreenGlobalStylesContent() {
>
{ colorVariations?.length && (
-
+
) }
{ typographyVariations?.length && (
{
} }
>
- { decodeEntities( siteTitle ) }
-
-
{
aria-label={ __(
'View site (opens in a new tab)'
) }
- icon={ external }
- className="edit-site-site-hub__site-view-link"
- />
-
+ >
+ { decodeEntities( siteTitle ) }
+
+
+
+ { children }
+
+```
+
+_Parameters_
+
+- _props_ `Object`: The component props.
+- _props.post_ `[Object]`: The post object to edit. This is required.
+- _props.\_\_unstableTemplate_ `[Object]`: The template object wrapper the edited post. This is optional and can only be used when the post type supports templates (like posts and pages).
+- _props.settings_ `[Object]`: The settings object to use for the editor. This is optional and can be used to override the default settings.
+- _props.children_ `[Element]`: Children elements for which the BlockEditorProvider context should apply. This is optional.
+
+_Returns_
+
+- `JSX.Element`: The rendered EditorProvider component.
### EditorSnackbars
diff --git a/packages/editor/package.json b/packages/editor/package.json
index d6d5731ebbc1f..620059721fdca 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -70,6 +70,7 @@
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"deepmerge": "^4.3.0",
+ "fast-deep-equal": "^3.1.3",
"is-plain-object": "^5.0.0",
"memize": "^2.1.0",
"react-autosize-textarea": "^7.1.0",
diff --git a/packages/editor/src/bindings/pattern-overrides.js b/packages/editor/src/bindings/pattern-overrides.js
index 4065cefe36280..107ed72e722ba 100644
--- a/packages/editor/src/bindings/pattern-overrides.js
+++ b/packages/editor/src/bindings/pattern-overrides.js
@@ -89,7 +89,5 @@ export default {
},
} );
},
- lockAttributesEditing() {
- return false;
- },
+ canUserEditValue: () => true,
};
diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js
index f5b3b526dbfd4..aec890c5ceff8 100644
--- a/packages/editor/src/bindings/post-meta.js
+++ b/packages/editor/src/bindings/post-meta.js
@@ -16,13 +16,53 @@ export default {
return args.key;
},
getValue( { registry, context, args } ) {
- const postType = context.postType
- ? context.postType
- : registry.select( editorStore ).getCurrentPostType();
-
return registry
.select( coreDataStore )
- .getEditedEntityRecord( 'postType', postType, context.postId )
- .meta?.[ args.key ];
+ .getEditedEntityRecord(
+ 'postType',
+ context?.postType,
+ context?.postId
+ ).meta?.[ args.key ];
+ },
+ setValue( { registry, context, args, value } ) {
+ registry
+ .dispatch( coreDataStore )
+ .editEntityRecord( 'postType', context?.postType, context?.postId, {
+ meta: {
+ [ args.key ]: value,
+ },
+ } );
+ },
+ canUserEditValue( { select, context, args } ) {
+ const postType =
+ context?.postType || select( editorStore ).getCurrentPostType();
+
+ // Check that editing is happening in the post editor and not a template.
+ if ( postType === 'wp_template' ) {
+ return false;
+ }
+
+ // Check that the custom field is not protected and available in the REST API.
+ const isFieldExposed = !! select( coreDataStore ).getEntityRecord(
+ 'postType',
+ postType,
+ context?.postId
+ )?.meta?.[ args.key ];
+
+ if ( ! isFieldExposed ) {
+ return false;
+ }
+
+ // Check that the user has the capability to edit post meta.
+ const canUserEdit = select( coreDataStore ).canUserEditEntityRecord(
+ 'postType',
+ context?.postType,
+ context?.postId
+ );
+ if ( ! canUserEdit ) {
+ return false;
+ }
+
+ return true;
},
};
diff --git a/packages/editor/src/components/editor-interface/content-slot-fill.js b/packages/editor/src/components/editor-interface/content-slot-fill.js
new file mode 100644
index 0000000000000..1aab394e87230
--- /dev/null
+++ b/packages/editor/src/components/editor-interface/content-slot-fill.js
@@ -0,0 +1,15 @@
+/**
+ * WordPress dependencies
+ */
+import { privateApis as componentsPrivateApis } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../lock-unlock';
+
+const { createPrivateSlotFill } = unlock( componentsPrivateApis );
+const SLOT_FILL_NAME = 'EditCanvasContainerSlot';
+const EditorContentSlotFill = createPrivateSlotFill( SLOT_FILL_NAME );
+
+export default EditorContentSlotFill;
diff --git a/packages/editor/src/components/editor-interface/index.js b/packages/editor/src/components/editor-interface/index.js
index 60ee3043e073b..b78e3caa0b57c 100644
--- a/packages/editor/src/components/editor-interface/index.js
+++ b/packages/editor/src/components/editor-interface/index.js
@@ -24,13 +24,13 @@ import { useState, useCallback } from '@wordpress/element';
*/
import { store as editorStore } from '../../store';
import EditorNotices from '../editor-notices';
-import EditorSnackbars from '../editor-snackbars';
import Header from '../header';
import InserterSidebar from '../inserter-sidebar';
import ListViewSidebar from '../list-view-sidebar';
import SavePublishPanels from '../save-publish-panels';
import TextEditor from '../text-editor';
import VisualEditor from '../visual-editor';
+import EditorContentSlotFill from './content-slot-fill';
const interfaceLabels = {
/* translators: accessibility text for the editor top bar landmark region. */
@@ -47,12 +47,17 @@ const interfaceLabels = {
export default function EditorInterface( {
className,
+ enableRegionNavigation,
styles,
children,
forceIsDirty,
contentRef,
disableIframe,
autoFocus,
+ customSaveButton,
+ forceDisableBlockTools,
+ title,
+ iframeProps,
} ) {
const {
mode,
@@ -60,6 +65,7 @@ export default function EditorInterface( {
isInserterOpened,
isListViewOpened,
isDistractionFree,
+ isPreviewMode,
previousShortcut,
nextShortcut,
showBlockBreadcrumbs,
@@ -77,6 +83,7 @@ export default function EditorInterface( {
isInserterOpened: select( editorStore ).isInserterOpened(),
isListViewOpened: select( editorStore ).isListViewOpened(),
isDistractionFree: get( 'core', 'distractionFree' ),
+ isPreviewMode: editorSettings.__unstableIsPreviewMode,
previousShortcut: select(
keyboardShortcutsStore
).getAllShortcutKeyCombinations( 'core/editor/previous-region' ),
@@ -112,6 +119,7 @@ export default function EditorInterface( {
return (
+ ! isPreviewMode && (
+
+ )
}
editorNotices={ }
secondarySidebar={
+ ! isPreviewMode &&
mode === 'visual' &&
( ( isInserterOpened && ) ||
( isListViewOpened && ) )
}
sidebar={
+ ! isPreviewMode &&
! isDistractionFree &&
}
- notices={ }
content={
<>
- { ! isDistractionFree && }
- { ( mode === 'text' || ! isRichEditingEnabled ) && (
-
- ) }
- { ! isLargeViewport && mode === 'visual' && (
-
+ { ! isDistractionFree && ! isPreviewMode && (
+
) }
- { isRichEditingEnabled && mode === 'visual' && (
-
- ) }
- { children }
+
+
+ { ( [ editorCanvasView ] ) =>
+ ! isPreviewMode && editorCanvasView ? (
+ editorCanvasView
+ ) : (
+ <>
+ { ! isPreviewMode &&
+ ( mode === 'text' ||
+ ! isRichEditingEnabled ) && (
+
+ ) }
+ { ! isPreviewMode &&
+ ! isLargeViewport &&
+ mode === 'visual' && (
+
+ ) }
+ { ( isPreviewMode ||
+ ( isRichEditingEnabled &&
+ mode === 'visual' ) ) && (
+
+ ) }
+ { children }
+ >
+ )
+ }
+
>
}
footer={
+ ! isPreviewMode &&
! isDistractionFree &&
isLargeViewport &&
showBlockBreadcrumbs &&
diff --git a/packages/editor/src/components/entities-saved-states/entity-record-item.js b/packages/editor/src/components/entities-saved-states/entity-record-item.js
index 15bdc8c2f2284..49733489b0a11 100644
--- a/packages/editor/src/components/entities-saved-states/entity-record-item.js
+++ b/packages/editor/src/components/entities-saved-states/entity-record-item.js
@@ -1,25 +1,32 @@
/**
* WordPress dependencies
*/
-import { CheckboxControl, PanelRow } from '@wordpress/components';
+import { Icon, CheckboxControl, Flex, PanelRow } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
+import { connection } from '@wordpress/icons';
/**
* Internal dependencies
*/
import { store as editorStore } from '../../store';
+import { unlock } from '../../lock-unlock';
export default function EntityRecordItem( { record, checked, onChange } ) {
const { name, kind, title, key } = record;
// Handle templates that might use default descriptive titles.
- const entityRecordTitle = useSelect(
+ const { entityRecordTitle, hasPostMetaChanges } = useSelect(
( select ) => {
if ( 'postType' !== kind || 'wp_template' !== name ) {
- return title;
+ return {
+ entityRecordTitle: title,
+ hasPostMetaChanges: unlock(
+ select( editorStore )
+ ).hasPostMetaChanges( name, key ),
+ };
}
const template = select( coreStore ).getEditedEntityRecord(
@@ -27,23 +34,45 @@ export default function EntityRecordItem( { record, checked, onChange } ) {
name,
key
);
- return select( editorStore ).__experimentalGetTemplateInfo(
- template
- ).title;
+ return {
+ entityRecordTitle:
+ select( editorStore ).__experimentalGetTemplateInfo(
+ template
+ ).title,
+ hasPostMetaChanges: unlock(
+ select( editorStore )
+ ).hasPostMetaChanges( name, key ),
+ };
},
[ name, kind, title, key ]
);
return (
-
-
-
+ <>
+
+
+
+ { hasPostMetaChanges && (
+
+
+
+
+ { __( 'Post Meta.' ) }
+
+
+
+ ) }
+ >
);
}
diff --git a/packages/editor/src/components/entities-saved-states/style.scss b/packages/editor/src/components/entities-saved-states/style.scss
index f4e0c9f814c24..30317a5367f65 100644
--- a/packages/editor/src/components/entities-saved-states/style.scss
+++ b/packages/editor/src/components/entities-saved-states/style.scss
@@ -37,3 +37,16 @@
height: $header-height + $border-width;
}
}
+
+.entities-saved-states__post-meta {
+ margin-left: $grid-unit-30;
+ align-items: center;
+}
+
+.entities-saved-states__connections-icon {
+ flex-grow: 0;
+}
+
+.entities-saved-states__bindings-text {
+ flex-grow: 1;
+}
diff --git a/packages/editor/src/components/header/index.js b/packages/editor/src/components/header/index.js
index 477ce41c968a6..4f1df093176a7 100644
--- a/packages/editor/src/components/header/index.js
+++ b/packages/editor/src/components/header/index.js
@@ -30,10 +30,20 @@ import PostViewLink from '../post-view-link';
import PreviewDropdown from '../preview-dropdown';
import { store as editorStore } from '../../store';
-const slideY = {
- hidden: { y: '-50px' },
- distractionFreeInactive: { y: 0 },
- hover: { y: 0, transition: { type: 'tween', delay: 0.2 } },
+const toolbarVariations = {
+ distractionFreeDisabled: { y: '-50px' },
+ distractionFreeHover: { y: 0 },
+ distractionFreeHidden: { y: '-50px' },
+ visible: { y: 0 },
+ hidden: { y: 0 },
+};
+
+const backButtonVariations = {
+ distractionFreeDisabled: { x: '-100%' },
+ distractionFreeHover: { x: 0 },
+ distractionFreeHidden: { x: '-100%' },
+ visible: { x: 0 },
+ hidden: { x: 0 },
};
function Header( {
@@ -81,11 +91,16 @@ function Header( {
// as some plugins might be relying on its presence.
return (
-
+
+
+
{ ! customSaveButton && ! isPublishSidebarOpened && (
diff --git a/packages/editor/src/components/post-discussion/panel.js b/packages/editor/src/components/post-discussion/panel.js
index 4812b021a6e6b..718754d56857a 100644
--- a/packages/editor/src/components/post-discussion/panel.js
+++ b/packages/editor/src/components/post-discussion/panel.js
@@ -6,7 +6,6 @@ import {
Dropdown,
Button,
__experimentalVStack as VStack,
- __experimentalText as Text,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useState, useMemo } from '@wordpress/element';
@@ -81,7 +80,7 @@ function PostDiscussionToggle( { isOpen, onClick } ) {
aria-expanded={ isOpen }
onClick={ onClick }
>
- { label }
+ { label }
);
}
diff --git a/packages/editor/src/components/post-discussion/style.scss b/packages/editor/src/components/post-discussion/style.scss
index b1eae85140285..b2d65c9aa7cf3 100644
--- a/packages/editor/src/components/post-discussion/style.scss
+++ b/packages/editor/src/components/post-discussion/style.scss
@@ -13,9 +13,7 @@
}
}
.editor-post-discussion__panel-toggle {
- &.components-button {
- height: auto;
- }
+
.components-text {
color: inherit;
}
diff --git a/packages/editor/src/components/post-panel-row/style.scss b/packages/editor/src/components/post-panel-row/style.scss
index 22d0cbbb644d8..baa7d7dd98977 100644
--- a/packages/editor/src/components/post-panel-row/style.scss
+++ b/packages/editor/src/components/post-panel-row/style.scss
@@ -11,6 +11,9 @@
min-height: $grid-unit-40;
display: flex;
align-items: center;
+ padding: 6px 0; // Matches button to ensure alignment
+ line-height: $grid-unit-05 * 5;
+ hyphens: auto;
}
.editor-post-panel__row-control {
@@ -18,4 +21,16 @@
min-height: $grid-unit-40;
display: flex;
align-items: center;
+
+ .components-button {
+ max-width: 100%;
+ text-align: left;
+ text-wrap: pretty;
+ height: auto;
+ min-height: $button-size-compact;
+ }
+
+ .components-dropdown {
+ max-width: 100%;
+ }
}
diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js
index 355986fdf509d..8797c9d142825 100644
--- a/packages/editor/src/components/post-publish-button/index.js
+++ b/packages/editor/src/components/post-publish-button/index.js
@@ -11,6 +11,7 @@ import { compose } from '@wordpress/compose';
*/
import PublishButtonLabel from './label';
import { store as editorStore } from '../../store';
+import { unlock } from '../../lock-unlock';
const noop = () => {};
@@ -45,14 +46,24 @@ export class PostPublishButton extends Component {
createOnClick( callback ) {
return ( ...args ) => {
- const { hasNonPostEntityChanges, setEntitiesSavedStatesCallback } =
- this.props;
+ const {
+ hasNonPostEntityChanges,
+ hasPostMetaChanges,
+ setEntitiesSavedStatesCallback,
+ isPublished,
+ } = this.props;
// If a post with non-post entities is published, but the user
// elects to not save changes to the non-post entities, those
// entities will still be dirty when the Publish button is clicked.
// We also need to check that the `setEntitiesSavedStatesCallback`
// prop was passed. See https://github.com/WordPress/gutenberg/pull/37383
- if ( hasNonPostEntityChanges && setEntitiesSavedStatesCallback ) {
+ //
+ // TODO: Explore how to manage `hasPostMetaChanges` and pre-publish workflow properly.
+ if (
+ ( hasNonPostEntityChanges ||
+ ( hasPostMetaChanges && isPublished ) ) &&
+ setEntitiesSavedStatesCallback
+ ) {
// The modal for multiple entity saving will open,
// hold the callback for saving/publishing the post
// so that we can call it if the post entity is checked.
@@ -212,7 +223,8 @@ export default compose( [
isSavingNonPostEntityChanges,
getEditedPostAttribute,
getPostEdits,
- } = select( editorStore );
+ hasPostMetaChanges,
+ } = unlock( select( editorStore ) );
return {
isSaving: isSavingPost(),
isAutoSaving: isAutosavingPost(),
@@ -229,6 +241,7 @@ export default compose( [
postStatus: getEditedPostAttribute( 'status' ),
postStatusHasChanged: getPostEdits()?.status,
hasNonPostEntityChanges: hasNonPostEntityChanges(),
+ hasPostMetaChanges: hasPostMetaChanges(),
isSavingNonPostEntityChanges: isSavingNonPostEntityChanges(),
};
} ),
diff --git a/packages/editor/src/components/post-schedule/style.scss b/packages/editor/src/components/post-schedule/style.scss
index 0b9d64a9ce140..02ea48016f969 100644
--- a/packages/editor/src/components/post-schedule/style.scss
+++ b/packages/editor/src/components/post-schedule/style.scss
@@ -8,14 +8,3 @@
padding: $grid-unit-20;
}
}
-
-.editor-post-schedule__dialog-toggle.components-button {
- overflow: hidden;
- text-align: left;
- white-space: unset;
- height: auto;
- min-height: $button-size-compact;
-
- // The line height + the padding should be the same as the button size.
- line-height: inherit;
-}
diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js
index 18d4dd4481d49..751f1009ff25e 100644
--- a/packages/editor/src/components/post-title/index.native.js
+++ b/packages/editor/src/components/post-title/index.native.js
@@ -18,11 +18,67 @@ import { store as blockEditorStore, RichText } from '@wordpress/block-editor';
import { store as editorStore } from '@wordpress/editor';
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
+/** @typedef {import('./types').RichTextValue} RichTextValue */
+
/**
* Internal dependencies
*/
import styles from './style.scss';
+/**
+ * Inserts content with title
+ *
+ * This function processes the given content and title, updating the title
+ * and content based on certain conditions. If the content is an array of
+ * blocks, it will check the first block for a heading or paragraph to use
+ * as the title. If the content is a string, it will strip HTML and update
+ * the title and content accordingly.
+ *
+ * @param {string} title The post title.
+ * @param {Array | string} content The content to be processed. It can be an array of blocks or a string.
+ * @param {Function} onUpdateTitle Callback function to update the title.
+ * @param {Function} onUpdateContent Callback function to update the content.
+ * @param {RichTextValue} value The initial value object, default is an object with empty text.
+ */
+export function insertContentWithTitle(
+ title,
+ content,
+ onUpdateTitle,
+ onUpdateContent,
+ value = create( { text: '' } )
+) {
+ if ( ! content.length ) {
+ return;
+ }
+
+ if ( typeof content !== 'string' ) {
+ const [ firstBlock ] = content;
+
+ if (
+ ! title &&
+ ( firstBlock.name === 'core/heading' ||
+ firstBlock.name === 'core/paragraph' )
+ ) {
+ // Strip HTML to avoid unwanted HTML being added to the title.
+ // In the majority of cases it is assumed that HTML in the title
+ // is undesirable.
+ const contentNoHTML = stripHTML( firstBlock.attributes.content );
+ onUpdateTitle( contentNoHTML );
+ onUpdateContent( content.slice( 1 ) );
+ } else {
+ onUpdateContent( content );
+ }
+ } else {
+ // Strip HTML to avoid unwanted HTML being added to the title.
+ // In the majority of cases it is assumed that HTML in the title
+ // is undesirable.
+ const contentNoHTML = stripHTML( content );
+
+ const newValue = insert( value, create( { html: contentNoHTML } ) );
+ onUpdateTitle( toHTMLString( { value: newValue } ) );
+ }
+}
+
class PostTitle extends Component {
constructor( props ) {
super( props );
@@ -59,45 +115,24 @@ class PostTitle extends Component {
}
onPaste( { value, plainText, html } ) {
- const { title, onInsertBlockAfter, onUpdate } = this.props;
+ const {
+ title,
+ onInsertBlockAfter: onInsertBlocks,
+ onUpdate,
+ } = this.props;
const content = pasteHandler( {
HTML: html,
plainText,
} );
- if ( ! content.length ) {
- return;
- }
-
- if ( typeof content !== 'string' ) {
- const [ firstBlock ] = content;
-
- if (
- ! title &&
- ( firstBlock.name === 'core/heading' ||
- firstBlock.name === 'core/paragraph' )
- ) {
- // Strip HTML to avoid unwanted HTML being added to the title.
- // In the majority of cases it is assumed that HTML in the title
- // is undesirable.
- const contentNoHTML = stripHTML(
- firstBlock.attributes.content
- );
- onUpdate( contentNoHTML );
- onInsertBlockAfter( content.slice( 1 ) );
- } else {
- onInsertBlockAfter( content );
- }
- } else {
- // Strip HTML to avoid unwanted HTML being added to the title.
- // In the majority of cases it is assumed that HTML in the title
- // is undesirable.
- const contentNoHTML = stripHTML( content );
-
- const newValue = insert( value, create( { html: contentNoHTML } ) );
- onUpdate( toHTMLString( { value: newValue } ) );
- }
+ insertContentWithTitle(
+ title,
+ content,
+ onUpdate,
+ onInsertBlocks,
+ value
+ );
}
setRef( richText ) {
diff --git a/packages/editor/src/components/post-url/style.scss b/packages/editor/src/components/post-url/style.scss
index c622cfce33f90..a711402f1a82e 100644
--- a/packages/editor/src/components/post-url/style.scss
+++ b/packages/editor/src/components/post-url/style.scss
@@ -2,15 +2,6 @@
width: 100%;
}
-.components-button.editor-post-url__panel-toggle {
- display: block;
- max-width: 100%;
- overflow: hidden;
- text-align: left;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
.editor-post-url__panel-dialog .editor-post-url {
// sidebar width - popover padding - form margin
min-width: $sidebar-width - $grid-unit-20 - $grid-unit-20;
diff --git a/packages/editor/src/components/provider/README.md b/packages/editor/src/components/provider/README.md
deleted file mode 100644
index deaa9375bba74..0000000000000
--- a/packages/editor/src/components/provider/README.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# EditorProvider
-
-EditorProvider is a component which establishes a new post editing context, and serves as the entry point for a new post editor (or post with template editor).
-
-It supports a big number of post types, including post, page, templates, custom post types, patterns, template parts.
-
-All modification and changes are performed to the `@wordpress/core-data` store.
-
-## Props
-
-### `post`
-
-- **Type:** `Object`
-- **Required** `yes`
-
-The post object to edit
-
-### `__unstableTemplate`
-
-- **Type:** `Object`
-- **Required** `no`
-
-The template object wrapper the edited post. This is optional and can only be used when the post type supports templates (like posts and pages).
-
-### `settings`
-
-- **Type:** `Object`
-- **Required** `no`
-
-The settings object to use for the editor. This is optional and can be used to override the default settings.
-
-### `children`
-
-- **Type:** `Element`
-- **Required** `no`
-
-Children elements for which the BlockEditorProvider context should apply.
diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js
index 56c86968f0274..081b1cdfa0f1b 100644
--- a/packages/editor/src/components/provider/index.js
+++ b/packages/editor/src/components/provider/index.js
@@ -59,6 +59,12 @@ const NON_CONTEXTUAL_POST_TYPES = [
* @param {Array} post Block list.
* @param {boolean} template Whether the page content has focus (and the surrounding template is inert). If `true` return page content blocks. Default `false`.
* @param {string} mode Rendering mode.
+ *
+ * @example
+ * ```jsx
+ * const [ blocks, onInput, onChange ] = useBlockEditorProps( post, template, mode );
+ * ```
+ *
* @return {Array} Block editor props.
*/
function useBlockEditorProps( post, template, mode ) {
@@ -118,6 +124,32 @@ function useBlockEditorProps( post, template, mode ) {
];
}
+/**
+ * This component provides the editor context and manages the state of the block editor.
+ *
+ * @param {Object} props The component props.
+ * @param {Object} props.post The post object.
+ * @param {Object} props.settings The editor settings.
+ * @param {boolean} props.recovery Indicates if the editor is in recovery mode.
+ * @param {Array} props.initialEdits The initial edits for the editor.
+ * @param {Object} props.children The child components.
+ * @param {Object} [props.BlockEditorProviderComponent] The block editor provider component to use. Defaults to ExperimentalBlockEditorProvider.
+ * @param {Object} [props.__unstableTemplate] The template object.
+ *
+ * @example
+ * ```jsx
+ *
+ * { children }
+ *
+ *
+ * @return {Object} The rendered ExperimentalEditorProvider component.
+ */
export const ExperimentalEditorProvider = withRegistryProvider(
( {
post,
@@ -293,6 +325,36 @@ export const ExperimentalEditorProvider = withRegistryProvider(
}
);
+/**
+ * This component establishes a new post editing context, and serves as the entry point for a new post editor (or post with template editor).
+ *
+ * It supports a large number of post types, including post, page, templates,
+ * custom post types, patterns, template parts.
+ *
+ * All modification and changes are performed to the `@wordpress/core-data` store.
+ *
+ * @param {Object} props The component props.
+ * @param {Object} [props.post] The post object to edit. This is required.
+ * @param {Object} [props.__unstableTemplate] The template object wrapper the edited post.
+ * This is optional and can only be used when the post type supports templates (like posts and pages).
+ * @param {Object} [props.settings] The settings object to use for the editor.
+ * This is optional and can be used to override the default settings.
+ * @param {Element} [props.children] Children elements for which the BlockEditorProvider context should apply.
+ * This is optional.
+ *
+ * @example
+ * ```jsx
+ *
+ * { children }
+ *
+ * ```
+ *
+ * @return {JSX.Element} The rendered EditorProvider component.
+ */
export function EditorProvider( props ) {
return (
( {
@@ -200,6 +204,12 @@ class NativeEditorProvider extends Component {
this.onHardwareBackPress
);
+ this.subscriptionOnContentUpdate = subscribeToContentUpdate(
+ ( data ) => {
+ this.onContentUpdate( data );
+ }
+ );
+
// Request current block impressions from native app.
requestBlockTypeImpressions( ( storedImpressions ) => {
const impressions = { ...NEW_BLOCK_TYPES, ...storedImpressions };
@@ -263,6 +273,10 @@ class NativeEditorProvider extends Component {
if ( this.hardwareBackPressListener ) {
this.hardwareBackPressListener.remove();
}
+
+ if ( this.subscriptionOnContentUpdate ) {
+ this.subscriptionOnContentUpdate.remove();
+ }
}
getThemeColors( { rawStyles, rawFeatures } ) {
@@ -303,6 +317,21 @@ class NativeEditorProvider extends Component {
return false;
}
+ onContentUpdate( { content: rawContent } ) {
+ const {
+ editTitle,
+ onClearPostTitleSelection,
+ onInsertBlockAfter: onInsertBlocks,
+ title,
+ } = this.props;
+ const content = pasteHandler( {
+ plainText: rawContent,
+ } );
+
+ insertContentWithTitle( title, content, editTitle, onInsertBlocks );
+ onClearPostTitleSelection();
+ }
+
serializeToNativeAction() {
const title = this.props.title;
let html;
@@ -423,11 +452,13 @@ const ComposedNativeProvider = compose( [
resetEditorBlocks,
updateEditorSettings,
switchEditorMode,
+ togglePostTitleSelection,
} = dispatch( editorStore );
const {
clearSelectedBlock,
updateSettings,
insertBlock,
+ insertBlocks,
replaceBlock,
} = dispatch( blockEditorStore );
const { addEntities, receiveEntityRecords } = dispatch( coreStore );
@@ -439,6 +470,7 @@ const ComposedNativeProvider = compose( [
updateEditorSettings,
addEntities,
insertBlock,
+ insertBlocks,
createSuccessNotice,
createErrorNotice,
clearSelectedBlock,
@@ -454,6 +486,12 @@ const ComposedNativeProvider = compose( [
switchMode( mode ) {
switchEditorMode( mode );
},
+ onInsertBlockAfter( blocks ) {
+ insertBlocks( blocks, undefined, undefined, false );
+ },
+ onClearPostTitleSelection() {
+ togglePostTitleSelection( false );
+ },
replaceBlock,
};
} ),
diff --git a/packages/editor/src/components/save-publish-panels/index.js b/packages/editor/src/components/save-publish-panels/index.js
index 812b1dcc1df41..3ae871c354bb6 100644
--- a/packages/editor/src/components/save-publish-panels/index.js
+++ b/packages/editor/src/components/save-publish-panels/index.js
@@ -14,6 +14,7 @@ import PostPublishPanel from '../post-publish-panel';
import PluginPrePublishPanel from '../plugin-pre-publish-panel';
import PluginPostPublishPanel from '../plugin-post-publish-panel';
import { store as editorStore } from '../../store';
+import { unlock } from '../../lock-unlock';
const { Fill, Slot } = createSlotFill( 'ActionsPanel' );
@@ -27,12 +28,19 @@ export default function SavePublishPanels( {
} ) {
const { closePublishSidebar, togglePublishSidebar } =
useDispatch( editorStore );
- const { publishSidebarOpened, hasNonPostEntityChanges } = useSelect(
+ const {
+ publishSidebarOpened,
+ hasNonPostEntityChanges,
+ hasPostMetaChanges,
+ } = useSelect(
( select ) => ( {
publishSidebarOpened:
select( editorStore ).isPublishSidebarOpened(),
hasNonPostEntityChanges:
select( editorStore ).hasNonPostEntityChanges(),
+ hasPostMetaChanges: unlock(
+ select( editorStore )
+ ).hasPostMetaChanges(),
} ),
[]
);
@@ -54,7 +62,7 @@ export default function SavePublishPanels( {
PostPublishExtension={ PluginPostPublishPanel.Slot }
/>
);
- } else if ( hasNonPostEntityChanges ) {
+ } else if ( hasNonPostEntityChanges || hasPostMetaChanges ) {
unmountableContent = (
{
return [
- ...styles,
+ ...( styles ?? [] ),
{
css: `.is-root-container{display:flow-root;${
// Some themes will have `min-height: 100vh` for the root container,
diff --git a/packages/editor/src/components/visual-editor/style.scss b/packages/editor/src/components/visual-editor/style.scss
index 59f98450257f6..597769ff7fb78 100644
--- a/packages/editor/src/components/visual-editor/style.scss
+++ b/packages/editor/src/components/visual-editor/style.scss
@@ -29,6 +29,7 @@
font-size: $default-font-size;
padding: 6px 12px;
+ &.is-tertiary,
&.has-icon {
padding: 6px;
}
diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js
index 3f4c24b4b1e07..58e15135e13ac 100644
--- a/packages/editor/src/private-apis.js
+++ b/packages/editor/src/private-apis.js
@@ -9,32 +9,18 @@ import * as interfaceApis from '@wordpress/interface';
import { ExperimentalEditorProvider } from './components/provider';
import { lock } from './lock-unlock';
import { EntitiesSavedStatesExtensible } from './components/entities-saved-states';
-import useAutoSwitchEditorSidebars from './components/provider/use-auto-switch-editor-sidebars';
+import EditorContentSlotFill from './components/editor-interface/content-slot-fill';
import useBlockEditorSettings from './components/provider/use-block-editor-settings';
import BackButton from './components/header/back-button';
import EditorInterface from './components/editor-interface';
-import Header from './components/header';
import CreateTemplatePartModal from './components/create-template-part-modal';
-import InserterSidebar from './components/inserter-sidebar';
-import ListViewSidebar from './components/list-view-sidebar';
-import PatternOverridesPanel from './components/pattern-overrides-panel';
import PluginPostExcerpt from './components/post-excerpt/plugin';
-import PostPanelRow from './components/post-panel-row';
import PreferencesModal from './components/preferences-modal';
-import PostActions from './components/post-actions';
import { usePostActions } from './components/post-actions/actions';
-import PostCardPanel from './components/post-card-panel';
-import PostStatus from './components/post-status';
import ToolsMoreMenuGroup from './components/more-menu/tools-more-menu-group';
import ViewMoreMenuGroup from './components/more-menu/view-more-menu-group';
-import { PrivatePostExcerptPanel } from './components/post-excerpt/panel';
-import SavePublishPanels from './components/save-publish-panels';
-import PostContentInformation from './components/post-content-information';
-import PostLastEditedPanel from './components/post-last-edited-panel';
import ResizableEditor from './components/resizable-editor';
import Sidebar from './components/sidebar';
-import TextEditor from './components/text-editor';
-import VisualEditor from './components/visual-editor';
import {
mergeBaseAndUserConfigs,
GlobalStylesProvider,
@@ -48,33 +34,19 @@ lock( privateApis, {
BackButton,
ExperimentalEditorProvider,
EntitiesSavedStatesExtensible,
- GlobalStylesProvider,
EditorInterface,
- Header,
- InserterSidebar,
- ListViewSidebar,
+ EditorContentSlotFill,
+ GlobalStylesProvider,
mergeBaseAndUserConfigs,
- PatternOverridesPanel,
PluginPostExcerpt,
- PostActions,
- PostPanelRow,
PreferencesModal,
usePostActions,
- PostCardPanel,
- PostStatus,
ToolsMoreMenuGroup,
ViewMoreMenuGroup,
- PrivatePostExcerptPanel,
- SavePublishPanels,
- PostContentInformation,
- PostLastEditedPanel,
ResizableEditor,
Sidebar,
- TextEditor,
- VisualEditor,
// This is a temporary private API while we're updating the site editor to use EditorProvider.
- useAutoSwitchEditorSidebars,
useBlockEditorSettings,
interfaceStore,
...remainingInterfaceApis,
diff --git a/packages/editor/src/private-apis.native.js b/packages/editor/src/private-apis.native.js
index d6fb0894d04d6..7c302c9c87d3a 100644
--- a/packages/editor/src/private-apis.native.js
+++ b/packages/editor/src/private-apis.native.js
@@ -10,24 +10,12 @@ import VisualEditor from './components/visual-editor';
import { ExperimentalEditorProvider } from './components/provider';
import { lock } from './lock-unlock';
import { EntitiesSavedStatesExtensible } from './components/entities-saved-states';
-import useAutoSwitchEditorSidebars from './components/provider/use-auto-switch-editor-sidebars';
import useBlockEditorSettings from './components/provider/use-block-editor-settings';
-import InserterSidebar from './components/inserter-sidebar';
-import ListViewSidebar from './components/list-view-sidebar';
-import PatternOverridesPanel from './components/pattern-overrides-panel';
import PluginPostExcerpt from './components/post-excerpt/plugin';
-import PostPanelRow from './components/post-panel-row';
import PreferencesModal from './components/preferences-modal';
-import PostActions from './components/post-actions';
import { usePostActions } from './components/post-actions/actions';
-import PostCardPanel from './components/post-card-panel';
-import PostStatus from './components/post-status';
import ToolsMoreMenuGroup from './components/more-menu/tools-more-menu-group';
import ViewMoreMenuGroup from './components/more-menu/view-more-menu-group';
-import { PrivatePostExcerptPanel } from './components/post-excerpt/panel';
-import SavePublishPanels from './components/save-publish-panels';
-import PostContentInformation from './components/post-content-information';
-import PostLastEditedPanel from './components/post-last-edited-panel';
const { store: interfaceStore, ...remainingInterfaceApis } = interfaceApis;
@@ -36,25 +24,13 @@ lock( privateApis, {
VisualEditor,
ExperimentalEditorProvider,
EntitiesSavedStatesExtensible,
- InserterSidebar,
- ListViewSidebar,
- PatternOverridesPanel,
PluginPostExcerpt,
- PostActions,
- PostPanelRow,
PreferencesModal,
usePostActions,
- PostCardPanel,
- PostStatus,
ToolsMoreMenuGroup,
ViewMoreMenuGroup,
- PrivatePostExcerptPanel,
- SavePublishPanels,
- PostContentInformation,
- PostLastEditedPanel,
// This is a temporary private API while we're updating the site editor to use EditorProvider.
- useAutoSwitchEditorSidebars,
useBlockEditorSettings,
interfaceStore,
...remainingInterfaceApis,
diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js
index aa2af9172ff18..8a866b46a6cdd 100644
--- a/packages/editor/src/store/private-selectors.js
+++ b/packages/editor/src/store/private-selectors.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import fastDeepEqual from 'fast-deep-equal';
+
/**
* WordPress dependencies
*/
@@ -17,6 +22,7 @@ import { store as coreStore } from '@wordpress/core-data';
*/
import {
getRenderingMode,
+ getCurrentPost,
__experimentalGetDefaultTemplatePartAreas,
} from './selectors';
import { TEMPLATE_PART_POST_TYPE } from './constants';
@@ -135,3 +141,42 @@ export const getCurrentTemplateTemplateParts = createRegistrySelector(
return getFilteredTemplatePartBlocks( blocks, templateParts );
}
);
+
+/**
+ * Returns true if there are unsaved changes to the
+ * post's meta fields, and false otherwise.
+ *
+ * @param {Object} state Global application state.
+ * @param {string} postType The post type of the post.
+ * @param {number} postId The ID of the post.
+ *
+ * @return {boolean} Whether there are edits or not in the meta fields of the relevant post.
+ */
+export const hasPostMetaChanges = createRegistrySelector(
+ ( select ) => ( state, postType, postId ) => {
+ const { type: currentPostType, id: currentPostId } =
+ getCurrentPost( state );
+ // If no postType or postId is passed, use the current post.
+ const edits = select( coreStore ).getEntityRecordNonTransientEdits(
+ 'postType',
+ postType || currentPostType,
+ postId || currentPostId
+ );
+
+ if ( ! edits?.meta ) {
+ return false;
+ }
+
+ // Compare if anything apart from `footnotes` has changed.
+ const originalPostMeta = select( coreStore ).getEntityRecord(
+ 'postType',
+ postType || currentPostType,
+ postId || currentPostId
+ )?.meta;
+
+ return ! fastDeepEqual(
+ { ...originalPostMeta, footnotes: undefined },
+ { ...edits.meta, footnotes: undefined }
+ );
+ }
+);
diff --git a/packages/interface/src/components/interface-skeleton/index.js b/packages/interface/src/components/interface-skeleton/index.js
index dc04ccec6fe5e..ed4d98cdf7637 100644
--- a/packages/interface/src/components/interface-skeleton/index.js
+++ b/packages/interface/src/components/interface-skeleton/index.js
@@ -26,6 +26,11 @@ import {
import NavigableRegion from '../navigable-region';
const ANIMATION_DURATION = 0.25;
+const commonTransition = {
+ type: 'tween',
+ duration: ANIMATION_DURATION,
+ ease: [ 0.6, 0, 0.4, 1 ],
+};
function useHTMLClass( className ) {
useEffect( () => {
@@ -42,12 +47,30 @@ function useHTMLClass( className ) {
}
const headerVariants = {
- hidden: { opacity: 0 },
- hover: {
+ hidden: { opacity: 1, marginTop: -60 },
+ visible: { opacity: 1, marginTop: 0 },
+ distractionFreeHover: {
opacity: 1,
- transition: { type: 'tween', delay: 0.2, delayChildren: 0.2 },
+ marginTop: 0,
+ transition: {
+ ...commonTransition,
+ delay: 0.2,
+ delayChildren: 0.2,
+ },
+ },
+ distractionFreeHidden: {
+ opacity: 0,
+ marginTop: -60,
+ },
+ distractionFreeDisabled: {
+ opacity: 0,
+ marginTop: 0,
+ transition: {
+ ...commonTransition,
+ delay: 0.8,
+ delayChildren: 0.8,
+ },
},
- distractionFreeInactive: { opacity: 1, transition: { delay: 0 } },
};
function InterfaceSkeleton(
@@ -58,7 +81,6 @@ function InterfaceSkeleton(
editorNotices,
sidebar,
secondarySidebar,
- notices,
content,
actions,
labels,
@@ -114,36 +136,39 @@ function InterfaceSkeleton(
) }
>
- { !! header && (
-
- { header }
-
- ) }
+
+ { !! header && (
+
+ { header }
+
+ ) }
+
{ isDistractionFree && (
{ editorNotices }
@@ -184,11 +209,6 @@ function InterfaceSkeleton(
) }
- { !! notices && (
-
- { notices }
-
- ) }
{
+ if ( isOverridableBlock( block ) ) {
+ return true;
+ }
+ return hasOverridableBlocks( block.innerBlocks );
+ } );
+}
diff --git a/packages/patterns/src/components/pattern-overrides-controls.js b/packages/patterns/src/components/pattern-overrides-controls.js
index 3ece910a0df47..9869c5b072c85 100644
--- a/packages/patterns/src/components/pattern-overrides-controls.js
+++ b/packages/patterns/src/components/pattern-overrides-controls.js
@@ -9,66 +9,48 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
-import {
- PARTIAL_SYNCING_SUPPORTED_BLOCKS,
- PATTERN_OVERRIDES_BINDING_SOURCE,
-} from '../constants';
+import { PATTERN_OVERRIDES_BINDING_SOURCE } from '../constants';
import {
AllowOverridesModal,
DisallowOverridesModal,
} from './allow-overrides-modal';
-function removeBindings( bindings, syncedAttributes ) {
- let updatedBindings = {};
- for ( const attributeName of syncedAttributes ) {
- // Omit any bindings that's not the same source from the `updatedBindings` object.
- if (
- bindings?.[ attributeName ]?.source !==
- PATTERN_OVERRIDES_BINDING_SOURCE &&
- bindings?.[ attributeName ]?.source !== undefined
- ) {
- updatedBindings[ attributeName ] = bindings[ attributeName ];
- }
- }
+function removeBindings( bindings ) {
+ let updatedBindings = { ...bindings };
+ delete updatedBindings.__default;
if ( ! Object.keys( updatedBindings ).length ) {
updatedBindings = undefined;
}
return updatedBindings;
}
-function addBindings( bindings, syncedAttributes ) {
- const updatedBindings = { ...bindings };
- for ( const attributeName of syncedAttributes ) {
- if ( ! bindings?.[ attributeName ] ) {
- updatedBindings[ attributeName ] = {
- source: PATTERN_OVERRIDES_BINDING_SOURCE,
- };
- }
- }
- return updatedBindings;
+function addBindings( bindings ) {
+ return {
+ ...bindings,
+ __default: { source: PATTERN_OVERRIDES_BINDING_SOURCE },
+ };
}
-function PatternOverridesControls( { attributes, name, setAttributes } ) {
+function PatternOverridesControls( { attributes, setAttributes } ) {
const controlId = useId();
const [ showAllowOverridesModal, setShowAllowOverridesModal ] =
useState( false );
const [ showDisallowOverridesModal, setShowDisallowOverridesModal ] =
useState( false );
- const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ];
- const attributeSources = syncedAttributes.map(
- ( attributeName ) =>
- attributes.metadata?.bindings?.[ attributeName ]?.source
- );
- const isConnectedToOtherSources = attributeSources.every(
- ( source ) => source && source !== 'core/pattern-overrides'
- );
+ const hasName = !! attributes.metadata?.name;
+ const defaultBindings = attributes.metadata?.bindings?.__default;
+ const allowOverrides =
+ hasName && defaultBindings?.source === PATTERN_OVERRIDES_BINDING_SOURCE;
+ const isConnectedToOtherSources =
+ defaultBindings?.source &&
+ defaultBindings.source !== PATTERN_OVERRIDES_BINDING_SOURCE;
function updateBindings( isChecked, customName ) {
const prevBindings = attributes?.metadata?.bindings;
const updatedBindings = isChecked
- ? addBindings( prevBindings, syncedAttributes )
- : removeBindings( prevBindings, syncedAttributes );
+ ? addBindings( prevBindings )
+ : removeBindings( prevBindings );
const updatedMetadata = {
...attributes.metadata,
@@ -89,13 +71,6 @@ function PatternOverridesControls( { attributes, name, setAttributes } ) {
return null;
}
- const hasName = !! attributes.metadata?.name;
- const allowOverrides =
- hasName &&
- attributeSources.some(
- ( source ) => source === PATTERN_OVERRIDES_BINDING_SOURCE
- );
-
return (
<>
diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js
index 05417de2b2c66..0553378cb5604 100644
--- a/packages/patterns/src/private-apis.js
+++ b/packages/patterns/src/private-apis.js
@@ -11,7 +11,7 @@ import {
default as DuplicatePatternModal,
useDuplicatePatternProps,
} from './components/duplicate-pattern-modal';
-import { isOverridableBlock } from './api';
+import { isOverridableBlock, hasOverridableBlocks } from './api';
import RenamePatternModal from './components/rename-pattern-modal';
import PatternsMenuItems from './components';
import RenamePatternCategoryModal from './components/rename-pattern-category-modal';
@@ -34,6 +34,7 @@ lock( privateApis, {
CreatePatternModalContents,
DuplicatePatternModal,
isOverridableBlock,
+ hasOverridableBlocks,
useDuplicatePatternProps,
RenamePatternModal,
PatternsMenuItems,
diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java
index 4f7066a5bd47d..315765edddf10 100644
--- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java
+++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java
@@ -66,6 +66,7 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu
private static final String EVENT_NAME_ON_UNDO_PRESSED = "onUndoPressed";
private static final String EVENT_NAME_ON_REDO_PRESSED = "onRedoPressed";
+ private static final String EVENT_NAME_ON_CONTENT_UPDATE = "onContentUpdate";
private static final String MAP_KEY_UPDATE_HTML = "html";
private static final String MAP_KEY_UPDATE_TITLE = "title";
@@ -91,6 +92,7 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu
private static final String MAP_KEY_REPLACE_BLOCK_HTML = "html";
private static final String MAP_KEY_REPLACE_BLOCK_BLOCK_ID = "clientId";
+ private static final String MAP_KEY_UPDATE_CONTENT = "content";
public static final String MAP_KEY_FEATURED_IMAGE_ID = "featuredImageId";
public static final String MAP_KEY_IS_CONNECTED = "isConnected";
@@ -214,6 +216,13 @@ public void onRedoPressed() {
emitToJS(EVENT_NAME_ON_REDO_PRESSED, null);
}
+ public void onContentUpdate(String content) {
+ WritableMap writableMap = new WritableNativeMap();
+
+ writableMap.putString(MAP_KEY_UPDATE_CONTENT, content);
+ emitToJS(EVENT_NAME_ON_CONTENT_UPDATE, writableMap);
+ }
+
@ReactMethod
public void addListener(String eventName) {
// Keep: Required for RN built in Event Emitter Calls.
diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java
index 8677c1737c52f..4477dfc115b7c 100644
--- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java
+++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java
@@ -846,6 +846,10 @@ public void onRedoPressed() {
mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onRedoPressed();
}
+ public void onContentUpdate(String content) {
+ mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onContentUpdate(content);
+ }
+
public void setTitle(String title) {
mTitleInitialized = true;
mTitle = title;
diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js
index 50e21fe86a683..da16f75e161da 100644
--- a/packages/react-native-bridge/index.js
+++ b/packages/react-native-bridge/index.js
@@ -202,6 +202,19 @@ export function subscribeConnectionStatus( callback ) {
);
}
+/**
+ * Subscribes a callback function to the 'onContentUpdate' event.
+ * This event is triggered with content that will be passed to the block editor
+ * to be converted into blocks.
+ *
+ * @param {Function} callback The function to be called when the 'onContentUpdate' event is triggered.
+ * This function receives content plain text/markdown as an argument.
+ * @return {Object} The listener object that was added to the event.
+ */
+export function subscribeToContentUpdate( callback ) {
+ return gutenbergBridgeEvents.addListener( 'onContentUpdate', callback );
+}
+
export function requestConnectionStatus( callback ) {
return RNReactNativeGutenbergBridge.requestConnectionStatus( callback );
}
diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift
index 2b293e2191979..2273801f1eeb9 100644
--- a/packages/react-native-bridge/ios/Gutenberg.swift
+++ b/packages/react-native-bridge/ios/Gutenberg.swift
@@ -221,6 +221,11 @@ public class Gutenberg: UIResponder {
var data: [String: Any] = ["isConnected": isConnected]
bridgeModule.sendEventIfNeeded(.connectionStatusChange, body: data)
}
+
+ public func onContentUpdate(content: String) {
+ var payload: [String: Any] = ["content": content]
+ bridgeModule.sendEventIfNeeded(.onContentUpdate, body: payload)
+ }
private func properties(from editorSettings: GutenbergEditorSettings?) -> [String : Any] {
var settingsUpdates = [String : Any]()
diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift
index aa115331ec2d8..96c3a8f25e0cb 100644
--- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift
+++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift
@@ -422,6 +422,7 @@ extension RNReactNativeGutenbergBridge {
case onUndoPressed
case onRedoPressed
case connectionStatusChange
+ case onContentUpdate
}
public override func supportedEvents() -> [String]! {
diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md
index b5ec9c7c0c321..beb32d70e6072 100644
--- a/packages/react-native-editor/CHANGELOG.md
+++ b/packages/react-native-editor/CHANGELOG.md
@@ -11,6 +11,7 @@ For each user feature we should also add a importance categorization label to i
## Unreleased
- [*] Prevent deleting content when backspacing in the first Paragraph block [#62069]
+- [internal] Adds new bridge functionality for updating content [#61796]
## 1.119.0
- [internal] Remove circular dependencies within the components package [#61102]
diff --git a/phpunit/class-wp-theme-json-schema-test.php b/phpunit/class-wp-theme-json-schema-test.php
index 4259b8b5de6d4..24a7cf0e2e43d 100644
--- a/phpunit/class-wp-theme-json-schema-test.php
+++ b/phpunit/class-wp-theme-json-schema-test.php
@@ -223,6 +223,20 @@ public function test_migrate_v2_to_latest() {
),
),
),
+ 'spacing' => array(
+ 'spacingSizes' => array(
+ array(
+ 'name' => 'Small',
+ 'slug' => 20,
+ 'size' => '20px',
+ ),
+ array(
+ 'name' => 'Large',
+ 'slug' => 80,
+ 'size' => '80px',
+ ),
+ ),
+ ),
),
);
@@ -246,6 +260,21 @@ public function test_migrate_v2_to_latest() {
),
),
),
+ 'spacing' => array(
+ 'defaultSpacingSizes' => false,
+ 'spacingSizes' => array(
+ array(
+ 'name' => 'Small',
+ 'slug' => 20,
+ 'size' => '20px',
+ ),
+ array(
+ 'name' => 'Large',
+ 'slug' => 80,
+ 'size' => '80px',
+ ),
+ ),
+ ),
),
);
diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php
index 28401bb20d484..549898b3c8773 100644
--- a/phpunit/class-wp-theme-json-test.php
+++ b/phpunit/class-wp-theme-json-test.php
@@ -503,6 +503,11 @@ public function test_get_stylesheet() {
),
),
),
+ 'core/media-text' => array(
+ 'typography' => array(
+ 'textAlign' => 'center',
+ ),
+ ),
'core/post-date' => array(
'color' => array(
'text' => '#123456',
@@ -547,7 +552,7 @@ public function test_get_stylesheet() {
);
$variables = ':root{--wp--preset--color--grey: grey;--wp--preset--gradient--custom-gradient: linear-gradient(135deg,rgba(0,0,0) 0%,rgb(0,0,0) 100%);--wp--preset--font-size--small: 14px;--wp--preset--font-size--big: 41px;--wp--preset--font-family--arial: Arial, serif;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}';
- $styles = static::$base_styles . ':root :where(body){color: var(--wp--preset--color--grey);}:root :where(a:where(:not(.wp-element-button))){background-color: #333;color: #111;}:root :where(.wp-element-button, .wp-block-button__link){box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.66);}:root :where(.wp-block-cover){min-height: unset;aspect-ratio: 16/9;}:root :where(.wp-block-group){background: var(--wp--preset--gradient--custom-gradient);border-radius: 10px;min-height: 50vh;padding: 24px;}:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: #111;}:root :where(.wp-block-heading){color: #123456;}:root :where(.wp-block-heading a:where(:not(.wp-element-button))){background-color: #333;color: #111;font-size: 60px;}:root :where(.wp-block-post-date){color: #123456;}:root :where(.wp-block-post-date a:where(:not(.wp-element-button))){background-color: #777;color: #555;}:root :where(.wp-block-post-excerpt){column-count: 2;}:root :where(.wp-block-image){margin-bottom: 30px;}:root :where(.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder){border-top-left-radius: 10px;border-bottom-right-radius: 1em;}:root :where(.wp-block-image img, .wp-block-image .components-placeholder){filter: var(--wp--preset--duotone--custom-duotone);}';
+ $styles = static::$base_styles . ':root :where(body){color: var(--wp--preset--color--grey);}:root :where(a:where(:not(.wp-element-button))){background-color: #333;color: #111;}:root :where(.wp-element-button, .wp-block-button__link){box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.66);}:root :where(.wp-block-cover){min-height: unset;aspect-ratio: 16/9;}:root :where(.wp-block-group){background: var(--wp--preset--gradient--custom-gradient);border-radius: 10px;min-height: 50vh;padding: 24px;}:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: #111;}:root :where(.wp-block-heading){color: #123456;}:root :where(.wp-block-heading a:where(:not(.wp-element-button))){background-color: #333;color: #111;font-size: 60px;}:root :where(.wp-block-media-text){text-align: center;}:root :where(.wp-block-post-date){color: #123456;}:root :where(.wp-block-post-date a:where(:not(.wp-element-button))){background-color: #777;color: #555;}:root :where(.wp-block-post-excerpt){column-count: 2;}:root :where(.wp-block-image){margin-bottom: 30px;}:root :where(.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder){border-top-left-radius: 10px;border-bottom-right-radius: 1em;}:root :where(.wp-block-image img, .wp-block-image .components-placeholder){filter: var(--wp--preset--duotone--custom-duotone);}';
$presets = '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-custom-gradient-gradient-background{background: var(--wp--preset--gradient--custom-gradient) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-big-font-size{font-size: var(--wp--preset--font-size--big) !important;}.has-arial-font-family{font-family: var(--wp--preset--font-family--arial) !important;}';
$all = $variables . $styles . $presets;
@@ -4276,10 +4281,10 @@ public function test_set_spacing_sizes( $spacing_scale, $expected_output ) {
'spacingScale' => $spacing_scale,
),
),
- )
+ ),
+ 'default'
);
- $theme_json->set_spacing_sizes();
$this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) );
}
@@ -4300,7 +4305,7 @@ public function data_set_spacing_sizes() {
),
'expected_output' => array(
array(
- 'name' => '1',
+ 'name' => 'Medium',
'slug' => '50',
'size' => '4rem',
),
@@ -4316,12 +4321,12 @@ public function data_set_spacing_sizes() {
),
'expected_output' => array(
array(
- 'name' => '1',
+ 'name' => 'Medium',
'slug' => '50',
'size' => '4rem',
),
array(
- 'name' => '2',
+ 'name' => 'Large',
'slug' => '60',
'size' => '5.5rem',
),
@@ -4337,17 +4342,17 @@ public function data_set_spacing_sizes() {
),
'expected_output' => array(
array(
- 'name' => '1',
+ 'name' => 'Small',
'slug' => '40',
'size' => '2.5rem',
),
array(
- 'name' => '2',
+ 'name' => 'Medium',
'slug' => '50',
'size' => '4rem',
),
array(
- 'name' => '3',
+ 'name' => 'Large',
'slug' => '60',
'size' => '5.5rem',
),
@@ -4363,22 +4368,22 @@ public function data_set_spacing_sizes() {
),
'expected_output' => array(
array(
- 'name' => '1',
+ 'name' => 'Small',
'slug' => '40',
'size' => '2.5rem',
),
array(
- 'name' => '2',
+ 'name' => 'Medium',
'slug' => '50',
'size' => '4rem',
),
array(
- 'name' => '3',
+ 'name' => 'Large',
'slug' => '60',
'size' => '5.5rem',
),
array(
- 'name' => '4',
+ 'name' => 'X-Large',
'slug' => '70',
'size' => '7rem',
),
@@ -4394,27 +4399,27 @@ public function data_set_spacing_sizes() {
),
'expected_output' => array(
array(
- 'name' => '1',
+ 'name' => 'Small',
'slug' => '40',
'size' => '2.5rem',
),
array(
- 'name' => '2',
+ 'name' => 'Medium',
'slug' => '50',
'size' => '5rem',
),
array(
- 'name' => '3',
+ 'name' => 'Large',
'slug' => '60',
'size' => '7.5rem',
),
array(
- 'name' => '4',
+ 'name' => 'X-Large',
'slug' => '70',
'size' => '10rem',
),
array(
- 'name' => '5',
+ 'name' => '2X-Large',
'slug' => '80',
'size' => '12.5rem',
),
@@ -4430,27 +4435,27 @@ public function data_set_spacing_sizes() {
),
'expected_output' => array(
array(
- 'name' => '1',
+ 'name' => 'X-Small',
'slug' => '30',
'size' => '0.67rem',
),
array(
- 'name' => '2',
+ 'name' => 'Small',
'slug' => '40',
'size' => '1rem',
),
array(
- 'name' => '3',
+ 'name' => 'Medium',
'slug' => '50',
'size' => '1.5rem',
),
array(
- 'name' => '4',
+ 'name' => 'Large',
'slug' => '60',
'size' => '2.25rem',
),
array(
- 'name' => '5',
+ 'name' => 'X-Large',
'slug' => '70',
'size' => '3.38rem',
),
@@ -4466,27 +4471,27 @@ public function data_set_spacing_sizes() {
),
'expected_output' => array(
array(
- 'name' => '1',
+ 'name' => 'X-Small',
'slug' => '30',
'size' => '0.09rem',
),
array(
- 'name' => '2',
+ 'name' => 'Small',
'slug' => '40',
'size' => '0.38rem',
),
array(
- 'name' => '3',
+ 'name' => 'Medium',
'slug' => '50',
'size' => '1.5rem',
),
array(
- 'name' => '4',
+ 'name' => 'Large',
'slug' => '60',
'size' => '6rem',
),
array(
- 'name' => '5',
+ 'name' => 'X-Large',
'slug' => '70',
'size' => '24rem',
),
@@ -4555,9 +4560,6 @@ public function data_set_spacing_sizes() {
* @param array $expected_output Expected output from data provider.
*/
public function test_set_spacing_sizes_when_invalid( $spacing_scale, $expected_output ) {
- $this->expectException( Exception::class );
- $this->expectExceptionMessage( 'Some of the theme.json settings.spacing.spacingScale values are invalid' );
-
$theme_json = new WP_Theme_JSON_Gutenberg(
array(
'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA,
@@ -4566,19 +4568,10 @@ public function test_set_spacing_sizes_when_invalid( $spacing_scale, $expected_o
'spacingScale' => $spacing_scale,
),
),
- )
- );
-
- // Ensure PHPUnit 10 compatibility.
- set_error_handler(
- static function ( $errno, $errstr ) {
- restore_error_handler();
- throw new Exception( $errstr, $errno );
- },
- E_ALL
+ ),
+ 'default'
);
- $theme_json->set_spacing_sizes();
$this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) );
}
@@ -4597,7 +4590,7 @@ public function data_set_spacing_sizes_when_invalid() {
'mediumStep' => 4,
'unit' => 'rem',
),
- 'expected_output' => null,
+ 'expected_output' => array(),
),
'non numeric increment' => array(
'spacing_scale' => array(
@@ -4607,7 +4600,7 @@ public function data_set_spacing_sizes_when_invalid() {
'mediumStep' => 4,
'unit' => 'rem',
),
- 'expected_output' => null,
+ 'expected_output' => array(),
),
'non numeric steps' => array(
'spacing_scale' => array(
@@ -4617,7 +4610,7 @@ public function data_set_spacing_sizes_when_invalid() {
'mediumStep' => 4,
'unit' => 'rem',
),
- 'expected_output' => null,
+ 'expected_output' => array(),
),
'non numeric medium step' => array(
'spacing_scale' => array(
@@ -4627,7 +4620,7 @@ public function data_set_spacing_sizes_when_invalid() {
'mediumStep' => 'That which is just right',
'unit' => 'rem',
),
- 'expected_output' => null,
+ 'expected_output' => array(),
),
'missing unit value' => array(
'spacing_scale' => array(
@@ -4636,7 +4629,7 @@ public function data_set_spacing_sizes_when_invalid() {
'steps' => 5,
'mediumStep' => 4,
),
- 'expected_output' => null,
+ 'expected_output' => array(),
),
);
}
diff --git a/schemas/json/theme.json b/schemas/json/theme.json
index 6a3ec7e81d394..bdfea7279f67a 100644
--- a/schemas/json/theme.json
+++ b/schemas/json/theme.json
@@ -428,6 +428,11 @@
"type": "boolean",
"default": true
},
+ "defaultSpacingSizes": {
+ "description": "Allow users to choose space sizes from the default space size presets.",
+ "type": "boolean",
+ "default": true
+ },
"spacingSizes": {
"description": "Space size presets for the space size selector.\nGenerates a custom property (`--wp--preset--spacing--{slug}`) per preset value.",
"type": "array",
@@ -439,8 +444,9 @@
"type": "string"
},
"slug": {
- "description": "Unique identifier for the space size preset. For best cross theme compatibility these should be in the form '10','20','30','40','50','60', etc. with '50' representing the 'Medium' size step.",
- "type": "string"
+ "description": "Unique identifier for the space size preset. Will be sorted numerically. For best cross theme compatibility these should be in the form '10','20','30','40','50','60', etc. with '50' representing the 'Medium' size step.",
+ "type": "string",
+ "pattern": "^[0-9].*$"
},
"size": {
"description": "CSS space-size value, including units.",
@@ -463,16 +469,22 @@
"increment": {
"description": "The amount to increment each step by.",
"type": "number",
+ "exclusiveMinimum": true,
+ "minimum": 0,
"default": 1.5
},
"steps": {
"description": "Number of steps to generate in scale.",
"type": "integer",
+ "minimum": 1,
+ "maximum": 10,
"default": 7
},
"mediumStep": {
"description": "The value to medium setting in the scale.",
"type": "number",
+ "exclusiveMinimum": true,
+ "minimum": 0,
"default": 1.5
},
"unit": {
diff --git a/test/e2e/specs/editor/blocks/columns.spec.js b/test/e2e/specs/editor/blocks/columns.spec.js
index 8ddf7e9377ff2..e322a52eeba10 100644
--- a/test/e2e/specs/editor/blocks/columns.spec.js
+++ b/test/e2e/specs/editor/blocks/columns.spec.js
@@ -40,7 +40,7 @@ test.describe( 'Columns', () => {
// Verify Column
const inserterOptions = page.locator(
- 'role=region[name="Block Library"i] >> role=option'
+ 'role=region[name="Block Library"i] >> .block-editor-inserter__insertable-blocks-at-selection >> role=option'
);
await expect( inserterOptions ).toHaveCount( 1 );
await expect( inserterOptions ).toHaveText( 'Column' );
diff --git a/test/e2e/specs/editor/plugins/child-blocks.spec.js b/test/e2e/specs/editor/plugins/child-blocks.spec.js
index b3073b70a5409..0cd043c6a4610 100644
--- a/test/e2e/specs/editor/plugins/child-blocks.spec.js
+++ b/test/e2e/specs/editor/plugins/child-blocks.spec.js
@@ -48,9 +48,13 @@ test.describe( 'Child Blocks', () => {
const blockInserter = page
.getByRole( 'toolbar', { name: 'Document tools' } )
.getByRole( 'button', { name: 'Toggle block inserter' } );
- const blockLibrary = page.getByRole( 'region', {
- name: 'Block Library',
- } );
+ const blockLibrary = page
+ .getByRole( 'region', {
+ name: 'Block Library',
+ } )
+ .locator(
+ '.block-editor-inserter__insertable-blocks-at-selection'
+ );
await blockInserter.click();
await expect( blockLibrary ).toBeVisible();
@@ -82,9 +86,13 @@ test.describe( 'Child Blocks', () => {
const blockInserter = page
.getByRole( 'toolbar', { name: 'Document tools' } )
.getByRole( 'button', { name: 'Toggle block inserter' } );
- const blockLibrary = page.getByRole( 'region', {
- name: 'Block Library',
- } );
+ const blockLibrary = page
+ .getByRole( 'region', {
+ name: 'Block Library',
+ } )
+ .locator(
+ '.block-editor-inserter__insertable-blocks-at-selection'
+ );
await blockInserter.click();
await expect( blockLibrary ).toBeVisible();
diff --git a/test/e2e/specs/editor/plugins/inner-blocks-allowed-blocks.spec.js b/test/e2e/specs/editor/plugins/inner-blocks-allowed-blocks.spec.js
index eaf171adf9313..d2dc521f0196b 100644
--- a/test/e2e/specs/editor/plugins/inner-blocks-allowed-blocks.spec.js
+++ b/test/e2e/specs/editor/plugins/inner-blocks-allowed-blocks.spec.js
@@ -46,9 +46,13 @@ test.describe( 'Allowed Blocks Setting on InnerBlocks', () => {
const blockInserter = page
.getByRole( 'toolbar', { name: 'Document tools' } )
.getByRole( 'button', { name: 'Toggle block inserter' } );
- const blockLibrary = page.getByRole( 'region', {
- name: 'Block Library',
- } );
+ const blockLibrary = page
+ .getByRole( 'region', {
+ name: 'Block Library',
+ } )
+ .locator(
+ '.block-editor-inserter__insertable-blocks-at-selection'
+ );
await blockInserter.click();
await expect( blockLibrary ).toBeVisible();
@@ -89,9 +93,13 @@ test.describe( 'Allowed Blocks Setting on InnerBlocks', () => {
const blockInserter = page
.getByRole( 'toolbar', { name: 'Document tools' } )
.getByRole( 'button', { name: 'Toggle block inserter' } );
- const blockLibrary = page.getByRole( 'region', {
- name: 'Block Library',
- } );
+ const blockLibrary = page
+ .getByRole( 'region', {
+ name: 'Block Library',
+ } )
+ .locator(
+ '.block-editor-inserter__insertable-blocks-at-selection'
+ );
await blockInserter.click();
await expect( blockLibrary ).toBeVisible();
diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js
index 97b8579bb07ba..87e5b2f2e10b1 100644
--- a/test/e2e/specs/editor/various/block-bindings.spec.js
+++ b/test/e2e/specs/editor/various/block-bindings.spec.js
@@ -1193,11 +1193,6 @@ test.describe( 'Block bindings', () => {
await expect( paragraphBlock ).toHaveText(
'Value of the text_custom_field'
);
- // Paragraph is not editable.
- await expect( paragraphBlock ).toHaveAttribute(
- 'contenteditable',
- 'false'
- );
// Check the frontend shows the value of the custom field.
const postId = await editor.publishPost();
@@ -1331,6 +1326,12 @@ test.describe( 'Block bindings', () => {
},
},
} );
+ // Select the paragraph and press Enter at the end of it.
+ const paragraph = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ await editor.selectBlocks( paragraph );
+ await page.keyboard.press( 'End' );
await page.keyboard.press( 'Enter' );
const [ initialParagraph, newEmptyParagraph ] =
await editor.canvas
@@ -1342,6 +1343,70 @@ test.describe( 'Block bindings', () => {
await expect( newEmptyParagraph ).toHaveText( '' );
await expect( newEmptyParagraph ).toBeEditable();
} );
+
+ test( 'should NOT be possible to edit the value of the custom field when it is protected', async ( {
+ editor,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ anchor: 'protected-field-binding',
+ content: 'fallback value',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: '_protected_field' },
+ },
+ },
+ },
+ },
+ } );
+
+ const protectedFieldBlock = editor.canvas.getByRole(
+ 'document',
+ {
+ name: 'Block: Paragraph',
+ }
+ );
+
+ await expect( protectedFieldBlock ).toHaveAttribute(
+ 'contenteditable',
+ 'false'
+ );
+ } );
+
+ test( 'should NOT be possible to edit the value of the custom field when it is not shown in the REST API', async ( {
+ editor,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ anchor: 'show-in-rest-false-binding',
+ content: 'fallback value',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: 'show_in_rest_false_field' },
+ },
+ },
+ },
+ },
+ } );
+
+ const showInRestFalseBlock = editor.canvas.getByRole(
+ 'document',
+ {
+ name: 'Block: Paragraph',
+ }
+ );
+
+ await expect( showInRestFalseBlock ).toHaveAttribute(
+ 'contenteditable',
+ 'false'
+ );
+ } );
} );
test.describe( 'Heading', () => {
@@ -1370,11 +1435,6 @@ test.describe( 'Block bindings', () => {
await expect( headingBlock ).toHaveText(
'Value of the text_custom_field'
);
- // Heading is not editable.
- await expect( headingBlock ).toHaveAttribute(
- 'contenteditable',
- 'false'
- );
// Check the frontend shows the value of the custom field.
const postId = await editor.publishPost();
@@ -1406,6 +1466,13 @@ test.describe( 'Block bindings', () => {
},
},
} );
+
+ // Select the heading and press Enter at the end of it.
+ const heading = editor.canvas.getByRole( 'document', {
+ name: 'Block: Heading',
+ } );
+ await editor.selectBlocks( heading );
+ await page.keyboard.press( 'End' );
await page.keyboard.press( 'Enter' );
// Can't use `editor.getBlocks` because it doesn't return the meta value shown in the editor.
const [ initialHeading, newEmptyParagraph ] =
@@ -1465,12 +1532,6 @@ test.describe( 'Block bindings', () => {
'Value of the text_custom_field'
);
- // Button is not editable.
- await expect( buttonBlock ).toHaveAttribute(
- 'contenteditable',
- 'false'
- );
-
// Check the frontend shows the value of the custom field.
const postId = await editor.publishPost();
await page.goto( `/?p=${ postId }` );
@@ -1599,6 +1660,7 @@ test.describe( 'Block bindings', () => {
} )
.getByRole( 'textbox' )
.click();
+ await page.keyboard.press( 'End' );
await page.keyboard.press( 'Enter' );
const [ initialButton, newEmptyButton ] = await editor.canvas
.locator( '[data-type="core/button"]' )
@@ -1723,12 +1785,7 @@ test.describe( 'Block bindings', () => {
imagePlaceholderSrc
);
- // Alt textarea is disabled and with the custom field value.
- await expect(
- page
- .getByRole( 'tabpanel', { name: 'Settings' } )
- .getByLabel( 'Alternative text' )
- ).toHaveAttribute( 'readonly' );
+ // Alt textarea should have the custom field value.
const altValue = await page
.getByRole( 'tabpanel', { name: 'Settings' } )
.getByLabel( 'Alternative text' )
@@ -1789,7 +1846,7 @@ test.describe( 'Block bindings', () => {
imagePlaceholderSrc
);
- // Title input is disabled and with the custom field value.
+ // Title input should have the custom field value.
const advancedButton = page
.getByRole( 'tabpanel', { name: 'Settings' } )
.getByRole( 'button', {
@@ -1800,11 +1857,6 @@ test.describe( 'Block bindings', () => {
if ( isAdvancedPanelOpen === 'false' ) {
await advancedButton.click();
}
- await expect(
- page
- .getByRole( 'tabpanel', { name: 'Settings' } )
- .getByLabel( 'Title attribute' )
- ).toHaveAttribute( 'readonly' );
const titleValue = await page
.getByRole( 'tabpanel', { name: 'Settings' } )
.getByLabel( 'Title attribute' )
@@ -1869,19 +1921,14 @@ test.describe( 'Block bindings', () => {
imageCustomFieldSrc
);
- // Alt textarea is disabled and with the custom field value.
- await expect(
- page
- .getByRole( 'tabpanel', { name: 'Settings' } )
- .getByLabel( 'Alternative text' )
- ).toHaveAttribute( 'readonly' );
+ // Alt textarea should have the custom field value.
const altValue = await page
.getByRole( 'tabpanel', { name: 'Settings' } )
.getByLabel( 'Alternative text' )
.inputValue();
expect( altValue ).toBe( 'Value of the text_custom_field' );
- // Title input is enabled and with the original value.
+ // Title input should have the original value.
const advancedButton = page
.getByRole( 'tabpanel', { name: 'Settings' } )
.getByRole( 'button', {
@@ -1892,11 +1939,6 @@ test.describe( 'Block bindings', () => {
if ( isAdvancedPanelOpen === 'false' ) {
await advancedButton.click();
}
- await expect(
- page
- .getByRole( 'tabpanel', { name: 'Settings' } )
- .getByLabel( 'Title attribute' )
- ).toBeEnabled();
const titleValue = await page
.getByRole( 'tabpanel', { name: 'Settings' } )
.getByLabel( 'Title attribute' )
@@ -1922,6 +1964,208 @@ test.describe( 'Block bindings', () => {
);
} );
} );
+
+ test.describe( 'Edit custom fields', () => {
+ test( 'should be possible to edit the value of the custom field from the paragraph', async ( {
+ editor,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ anchor: 'paragraph-binding',
+ content: 'paragraph default content',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: 'text_custom_field' },
+ },
+ },
+ },
+ },
+ } );
+ const paragraphBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+
+ await expect( paragraphBlock ).toHaveAttribute(
+ 'contenteditable',
+ 'true'
+ );
+ await paragraphBlock.fill( 'new value' );
+ // Check that the paragraph content attribute didn't change.
+ const [ paragraphBlockObject ] = await editor.getBlocks();
+ expect( paragraphBlockObject.attributes.content ).toBe(
+ 'paragraph default content'
+ );
+ // Check the value of the custom field is being updated by visiting the frontend.
+ const previewPage = await editor.openPreviewPage();
+ await expect(
+ previewPage.locator( '#paragraph-binding' )
+ ).toHaveText( 'new value' );
+ } );
+
+ test( 'should be possible to edit the value of the url custom field from the button', async ( {
+ editor,
+ page,
+ pageUtils,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/buttons',
+ innerBlocks: [
+ {
+ name: 'core/button',
+ attributes: {
+ anchor: 'button-url-binding',
+ text: 'button default text',
+ url: '#default-url',
+ metadata: {
+ bindings: {
+ url: {
+ source: 'core/post-meta',
+ args: { key: 'url_custom_field' },
+ },
+ },
+ },
+ },
+ },
+ ],
+ } );
+
+ // Edit the url.
+ const buttonBlock = editor.canvas
+ .getByRole( 'document', {
+ name: 'Block: Button',
+ exact: true,
+ } )
+ .getByRole( 'textbox' );
+ await buttonBlock.click();
+ await page
+ .getByRole( 'button', { name: 'Edit link', exact: true } )
+ .click();
+ await page
+ .getByPlaceholder( 'Search or type url' )
+ .fill( '#url-custom-field-modified' );
+ await pageUtils.pressKeys( 'Enter' );
+
+ // Check that the button url attribute didn't change.
+ const [ buttonsObject ] = await editor.getBlocks();
+ expect( buttonsObject.innerBlocks[ 0 ].attributes.url ).toBe(
+ '#default-url'
+ );
+ // Check the value of the custom field is being updated by visiting the frontend.
+ const previewPage = await editor.openPreviewPage();
+ await expect(
+ previewPage.locator( '#button-url-binding a' )
+ ).toHaveAttribute( 'href', '#url-custom-field-modified' );
+ } );
+
+ test( 'should be possible to edit the value of the url custom field from the image', async ( {
+ editor,
+ page,
+ pageUtils,
+ requestUtils,
+ } ) => {
+ const customFieldMedia = await requestUtils.uploadMedia(
+ path.join(
+ './test/e2e/assets',
+ '1024x768_e2e_test_image_size.jpeg'
+ )
+ );
+ imageCustomFieldSrc = customFieldMedia.source_url;
+
+ await editor.insertBlock( {
+ name: 'core/image',
+ attributes: {
+ anchor: 'image-url-binding',
+ url: imagePlaceholderSrc,
+ alt: 'default alt value',
+ title: 'default title value',
+ metadata: {
+ bindings: {
+ url: {
+ source: 'core/post-meta',
+ args: { key: 'url_custom_field' },
+ },
+ },
+ },
+ },
+ } );
+
+ // Edit image url.
+ await page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', {
+ name: 'Replace',
+ } )
+ .click();
+ await page
+ .getByRole( 'button', { name: 'Edit link', exact: true } )
+ .click();
+ await page
+ .getByPlaceholder( 'Search or type url' )
+ .fill( imageCustomFieldSrc );
+ await pageUtils.pressKeys( 'Enter' );
+
+ // Check that the image url attribute didn't change and still uses the placeholder.
+ const [ imageBlockObject ] = await editor.getBlocks();
+ expect( imageBlockObject.attributes.url ).toBe(
+ imagePlaceholderSrc
+ );
+
+ // Check the value of the custom field is being updated by visiting the frontend.
+ const previewPage = await editor.openPreviewPage();
+ await expect(
+ previewPage.locator( '#image-url-binding img' )
+ ).toHaveAttribute( 'src', imageCustomFieldSrc );
+ } );
+
+ test( 'should be possible to edit the value of the text custom field from the image alt', async ( {
+ editor,
+ page,
+ } ) => {
+ await editor.insertBlock( {
+ name: 'core/image',
+ attributes: {
+ anchor: 'image-alt-binding',
+ url: imagePlaceholderSrc,
+ alt: 'default alt value',
+ metadata: {
+ bindings: {
+ alt: {
+ source: 'core/post-meta',
+ args: { key: 'text_custom_field' },
+ },
+ },
+ },
+ },
+ } );
+ const imageBlockImg = editor.canvas
+ .getByRole( 'document', {
+ name: 'Block: Image',
+ } )
+ .locator( 'img' );
+ await imageBlockImg.click();
+
+ // Edit the custom field value in the alt textarea.
+ const altInputArea = page
+ .getByRole( 'tabpanel', { name: 'Settings' } )
+ .getByLabel( 'Alternative text' );
+ await expect( altInputArea ).not.toHaveAttribute( 'readonly' );
+ await altInputArea.fill( 'new value' );
+
+ // Check that the image alt attribute didn't change.
+ const [ imageBlockObject ] = await editor.getBlocks();
+ expect( imageBlockObject.attributes.alt ).toBe(
+ 'default alt value'
+ );
+ // Check the value of the custom field is being updated by visiting the frontend.
+ const previewPage = await editor.openPreviewPage();
+ await expect(
+ previewPage.locator( '#image-alt-binding img' )
+ ).toHaveAttribute( 'alt', 'new value' );
+ } );
+ } );
} );
} );
diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js
index 976f1c378daa9..f4648a03efe95 100644
--- a/test/e2e/specs/editor/various/pattern-overrides.spec.js
+++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js
@@ -105,7 +105,7 @@ test.describe( 'Pattern Overrides', () => {
metadata: {
name: editableParagraphName,
bindings: {
- content: {
+ __default: {
source: 'core/pattern-overrides',
},
},
@@ -234,7 +234,7 @@ test.describe( 'Pattern Overrides', () => {
const paragraphName = 'paragraph-name';
const { id } = await requestUtils.createBlock( {
title: 'Pattern',
- content: `
+ content: `
Editable
`,
status: 'publish',
@@ -324,7 +324,7 @@ test.describe( 'Pattern Overrides', () => {
const { id } = await requestUtils.createBlock( {
title: 'Button with target',
content: `
-
+
`,
@@ -434,14 +434,14 @@ test.describe( 'Pattern Overrides', () => {
const headingName = 'Editable heading';
const innerPattern = await requestUtils.createBlock( {
title: 'Inner Pattern',
- content: `
+ content: `
Inner paragraph
`,
status: 'publish',
} );
const outerPattern = await requestUtils.createBlock( {
title: 'Outer Pattern',
- content: `
+ content: `
Outer heading
`,
@@ -535,10 +535,10 @@ test.describe( 'Pattern Overrides', () => {
const paragraphName = 'Editable paragraph';
const { id } = await requestUtils.createBlock( {
title: 'Pattern',
- content: `
+ content: `
Heading
-
+
Paragraph
`,
status: 'publish',
@@ -694,7 +694,7 @@ test.describe( 'Pattern Overrides', () => {
);
const { id } = await requestUtils.createBlock( {
title: 'Pattern',
- content: `
+ content: `
`,
status: 'publish',
@@ -878,4 +878,82 @@ test.describe( 'Pattern Overrides', () => {
editorSettings.getByRole( 'button', { name: 'Enable overrides' } )
).toBeHidden();
} );
+
+ // @see https://github.com/WordPress/gutenberg/pull/60694
+ test( 'handles back-compat from individual attributes to __default', async ( {
+ page,
+ admin,
+ requestUtils,
+ editor,
+ } ) => {
+ const imageName = 'Editable image';
+ const TEST_IMAGE_FILE_PATH = path.resolve(
+ __dirname,
+ '../../../assets/10x10_e2e_test_image_z9T8jK.png'
+ );
+ const { id } = await requestUtils.createBlock( {
+ title: 'Pattern',
+ content: `
+
+`,
+ status: 'publish',
+ } );
+
+ await admin.createNewPost();
+
+ await editor.insertBlock( {
+ name: 'core/block',
+ attributes: { ref: id },
+ } );
+
+ const blocks = await editor.getBlocks( { full: true } );
+ expect( blocks ).toMatchObject( [
+ {
+ name: 'core/block',
+ attributes: { ref: id },
+ },
+ ] );
+ expect(
+ await editor.getBlocks( { clientId: blocks[ 0 ].clientId } )
+ ).toMatchObject( [
+ {
+ name: 'core/image',
+ attributes: {
+ metadata: {
+ name: imageName,
+ bindings: {
+ __default: {
+ source: 'core/pattern-overrides',
+ },
+ },
+ },
+ },
+ },
+ ] );
+
+ const imageBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Image',
+ } );
+ await editor.selectBlocks( imageBlock );
+ await imageBlock
+ .getByTestId( 'form-file-upload-input' )
+ .setInputFiles( TEST_IMAGE_FILE_PATH );
+ await expect( imageBlock.getByRole( 'img' ) ).toHaveCount( 1 );
+ await expect( imageBlock.getByRole( 'img' ) ).toHaveAttribute(
+ 'src',
+ /\/wp-content\/uploads\//
+ );
+ await editor.showBlockToolbar();
+ await editor.clickBlockToolbarButton( 'Alt' );
+ await page
+ .getByRole( 'textbox', { name: 'alternative text' } )
+ .fill( 'Test Image' );
+
+ const postId = await editor.publishPost();
+
+ await page.goto( `/?p=${ postId }` );
+ await expect(
+ page.getByRole( 'img', { name: 'Test Image' } )
+ ).toHaveAttribute( 'src', /\/wp-content\/uploads\// );
+ } );
} );
diff --git a/test/e2e/specs/editor/various/patterns.spec.js b/test/e2e/specs/editor/various/patterns.spec.js
index 77f97ee3004a9..781d4657a20b9 100644
--- a/test/e2e/specs/editor/various/patterns.spec.js
+++ b/test/e2e/specs/editor/various/patterns.spec.js
@@ -388,6 +388,13 @@ test.describe( 'Synced pattern', () => {
.getByRole( 'button', { name: 'Add' } )
.click();
+ // Wait until the pattern is created.
+ await expect(
+ editor.canvas.getByRole( 'document', {
+ name: 'Block: Pattern',
+ } )
+ ).toBeVisible();
+
await admin.createNewPost();
await editor.canvas
.getByRole( 'button', { name: 'Add default block' } )
@@ -432,11 +439,11 @@ test.describe( 'Synced pattern', () => {
.click();
// Wait until the pattern is created.
- await editor.canvas
- .getByRole( 'document', {
+ await expect(
+ editor.canvas.getByRole( 'document', {
name: 'Block: Pattern',
} )
- .waitFor();
+ ).toBeVisible();
// Check that only the pattern block is present.
const existingBlocks = await editor.getBlocks();
diff --git a/test/e2e/specs/editor/various/publish-panel.spec.js b/test/e2e/specs/editor/various/publish-panel.spec.js
index 534fea5289c9e..c70a7b85cd70f 100644
--- a/test/e2e/specs/editor/various/publish-panel.spec.js
+++ b/test/e2e/specs/editor/various/publish-panel.spec.js
@@ -113,4 +113,98 @@ test.describe( 'Post publish panel', () => {
} )
).toBeFocused();
} );
+
+ test( 'should show panel and indicator when metadata has been modified', async ( {
+ admin,
+ editor,
+ page,
+ } ) => {
+ await admin.createNewPost( {
+ title: 'Test metadata changes with save panel',
+ } );
+ await editor.insertBlock( {
+ name: 'core/paragraph',
+ attributes: {
+ content: 'paragraph default content',
+ metadata: {
+ bindings: {
+ content: {
+ source: 'core/post-meta',
+ args: { key: 'text_custom_field' },
+ },
+ },
+ },
+ },
+ } );
+ const postId = await editor.publishPost();
+ const metadataUtils = new MetadataUtils( page );
+ await metadataUtils.modifyPostMetadata(
+ 'post',
+ postId,
+ 'text_custom_field',
+ 'test value'
+ );
+ const editorTopBar = page.getByRole( 'region', {
+ name: 'Editor top bar',
+ } );
+
+ const saveButton = editorTopBar.getByRole( 'button', {
+ name: 'Save',
+ exact: true,
+ } );
+
+ await expect( saveButton ).toBeVisible();
+
+ await saveButton.click();
+
+ const publishPanel = page.getByRole( 'region', {
+ name: 'Editor publish',
+ } );
+
+ await expect( publishPanel ).toBeVisible();
+
+ const postMetaPanel = publishPanel.locator(
+ '.entities-saved-states__post-meta'
+ );
+
+ await expect( postMetaPanel ).toBeVisible();
+ } );
} );
+
+/**
+ * Utilities for working with metadata.
+ *
+ * @param postType
+ * @param postId
+ * @param metaKey
+ * @param metaValue
+ */
+class MetadataUtils {
+ constructor( page ) {
+ this.page = page;
+ }
+
+ async modifyPostMetadata( postType, postId, metaKey, metaValue ) {
+ const parameters = {
+ postType,
+ postId,
+ metaKey,
+ metaValue,
+ };
+
+ await this.page.evaluate( ( _parameters ) => {
+ window.wp.data
+ .dispatch( 'core' )
+ .editEntityRecord(
+ 'postType',
+ _parameters.postType,
+ _parameters.postId,
+ {
+ meta: {
+ [ _parameters.metaKey ]: _parameters.metaValue,
+ },
+ }
+ );
+ }, parameters );
+ }
+}
diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js
index 2dee26255c103..2eee3cad2b73f 100644
--- a/test/e2e/specs/editor/various/writing-flow.spec.js
+++ b/test/e2e/specs/editor/various/writing-flow.spec.js
@@ -595,6 +595,37 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => {
] );
} );
+ test( 'should remove first empty paragraph on Backspace', async ( {
+ editor,
+ page,
+ } ) => {
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.press( 'Enter' );
+ await page.keyboard.type( '2' );
+ await page.keyboard.press( 'ArrowUp' );
+
+ // Ensure setup is correct.
+ expect( await editor.getBlocks() ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: { content: '' },
+ },
+ {
+ name: 'core/paragraph',
+ attributes: { content: '2' },
+ },
+ ] );
+
+ await page.keyboard.press( 'Backspace' );
+
+ expect( await editor.getBlocks() ).toMatchObject( [
+ {
+ name: 'core/paragraph',
+ attributes: { content: '2' },
+ },
+ ] );
+ } );
+
test( 'should merge paragraphs', async ( { editor, page } ) => {
await page.keyboard.press( 'Enter' );
await page.keyboard.type( '1' );
diff --git a/test/e2e/specs/site-editor/block-removal.spec.js b/test/e2e/specs/site-editor/block-removal.spec.js
index 14f0ae422cc98..7fc547c19e59e 100644
--- a/test/e2e/specs/site-editor/block-removal.spec.js
+++ b/test/e2e/specs/site-editor/block-removal.spec.js
@@ -30,7 +30,9 @@ test.describe( 'Site editor block removal prompt', () => {
.click();
// Select and try to remove Query Loop block
- const listView = page.getByRole( 'region', { name: 'List View' } );
+ const listView = page.getByRole( 'region', {
+ name: 'Document Overview',
+ } );
await listView.getByRole( 'link', { name: 'Query Loop' } ).click();
await page.keyboard.press( 'Backspace' );
@@ -52,7 +54,9 @@ test.describe( 'Site editor block removal prompt', () => {
.click();
// Select and open child blocks of Query Loop block
- const listView = page.getByRole( 'region', { name: 'List View' } );
+ const listView = page.getByRole( 'region', {
+ name: 'Document Overview',
+ } );
await listView.getByRole( 'link', { name: 'Query Loop' } ).click();
await page.keyboard.press( 'ArrowRight' );
@@ -79,7 +83,9 @@ test.describe( 'Site editor block removal prompt', () => {
.click();
// Select Query Loop list item
- const listView = page.getByRole( 'region', { name: 'List View' } );
+ const listView = page.getByRole( 'region', {
+ name: 'Document Overview',
+ } );
await listView.getByRole( 'link', { name: 'Query Loop' } ).click();
// Reveal its inner blocks in the list view
diff --git a/test/e2e/specs/site-editor/list-view.spec.js b/test/e2e/specs/site-editor/list-view.spec.js
index ba8a316ee10c4..db514463a73d7 100644
--- a/test/e2e/specs/site-editor/list-view.spec.js
+++ b/test/e2e/specs/site-editor/list-view.spec.js
@@ -26,7 +26,7 @@ test.describe( 'Site Editor List View', () => {
editor,
} ) => {
await expect(
- page.locator( 'role=region[name="List View"i]' )
+ page.locator( 'role=region[name="Document Overview"i]' )
).toBeHidden();
// Turn on block list view by default.
@@ -37,7 +37,7 @@ test.describe( 'Site Editor List View', () => {
await page.reload();
await expect(
- page.locator( 'role=region[name="List View"i]' )
+ page.locator( 'role=region[name="Document Overview"i]' )
).toBeVisible();
// The preferences cleanup.
@@ -121,7 +121,7 @@ test.describe( 'Site Editor List View', () => {
await pageUtils.pressKeys( 'shift+Tab' );
await expect(
page
- .getByRole( 'region', { name: 'List View' } )
+ .getByRole( 'region', { name: 'Document Overview' } )
.getByRole( 'button', {
name: 'Close',
} )
diff --git a/test/e2e/specs/site-editor/navigation-editor.spec.js b/test/e2e/specs/site-editor/navigation-editor.spec.js
index 47519dc3ba613..2813ceb13748a 100644
--- a/test/e2e/specs/site-editor/navigation-editor.spec.js
+++ b/test/e2e/specs/site-editor/navigation-editor.spec.js
@@ -45,7 +45,7 @@ test.describe( 'Editing Navigation Menus', () => {
const listView = page
.getByRole( 'region', {
- name: 'List View',
+ name: 'Document Overview',
} )
.getByRole( 'treegrid', {
name: 'Block navigation structure',
@@ -99,7 +99,7 @@ test.describe( 'Editing Navigation Menus', () => {
// Check the standard tabs are not present.
await expect(
- sidebar.getByRole( 'tab', { name: 'List View' } )
+ sidebar.getByRole( 'tab', { name: 'Document Overview' } )
).toBeHidden();
await expect(
sidebar.getByRole( 'tab', { name: 'Settings' } )
diff --git a/test/native/setup.js b/test/native/setup.js
index 12b61d15553ca..e93d2248bd03c 100644
--- a/test/native/setup.js
+++ b/test/native/setup.js
@@ -109,6 +109,7 @@ jest.mock( '@wordpress/react-native-bridge', () => {
subscribeOnUndoPressed: jest.fn(),
subscribeOnRedoPressed: jest.fn(),
subscribeConnectionStatus: jest.fn( () => ( { remove: jest.fn() } ) ),
+ subscribeToContentUpdate: jest.fn(),
requestConnectionStatus: jest.fn( ( callback ) => callback( true ) ),
editorDidMount: jest.fn(),
showAndroidSoftKeyboard: jest.fn(),