diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md
index fd29b1c6e1388c..744b7b2896e26a 100644
--- a/docs/reference-guides/data/data-core-block-editor.md
+++ b/docs/reference-guides/data/data-core-block-editor.md
@@ -168,29 +168,6 @@ _Returns_
- `Array?`: The list of allowed block types.
-### getBehaviors
-
-Returns the behaviors registered with the editor.
-
-Behaviors are named, reusable pieces of functionality that can be attached to blocks. They are registered with the editor using the `theme.json` file.
-
-_Usage_
-
-```js
-const behaviors = select( blockEditorStore ).getBehaviors();
-if ( behaviors?.lightbox ) {
- // Do something with the lightbox.
-}
-```
-
-_Parameters_
-
-- _state_ `Object`: Editor state.
-
-_Returns_
-
-- `Object`: The editor behaviors object.
-
### getBlock
Returns a block given its client ID. This is a parsed copy of the block, containing its `blockName`, `clientId`, and current `attributes` state. This is not the block's registration settings, which must be retrieved from the blocks module registration store.
diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php
index c45ce23c5d4ea7..e9c73a717d3d0d 100644
--- a/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php
+++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php
@@ -21,7 +21,7 @@ class Gutenberg_REST_Global_Styles_Revisions_Controller_6_3 extends WP_REST_Cont
* @since 6.3.0
* @var string
*/
- private $parent_post_type;
+ protected $parent_post_type;
/**
* The base of the parent controller's route.
@@ -102,7 +102,7 @@ public function get_collection_params() {
* @param string $raw_json Encoded JSON from global styles custom post content.
* @return Array|WP_Error
*/
- private function get_decoded_global_styles_json( $raw_json ) {
+ protected function get_decoded_global_styles_json( $raw_json ) {
$decoded_json = json_decode( $raw_json, true );
if ( is_array( $decoded_json ) && isset( $decoded_json['isGlobalStylesUserThemeJSON'] ) && true === $decoded_json['isGlobalStylesUserThemeJSON'] ) {
diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php
index 144ad4d50c83f1..90898c0b71e246 100644
--- a/lib/compat/wordpress-6.3/rest-api.php
+++ b/lib/compat/wordpress-6.3/rest-api.php
@@ -52,24 +52,6 @@ function gutenberg_update_templates_template_parts_rest_controller( $args, $post
}
add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 );
-/**
- * Registers the Global Styles Revisions REST API routes.
- */
-function gutenberg_register_global_styles_revisions_endpoints() {
- $global_styles_revisions_controller = new Gutenberg_REST_Global_Styles_Revisions_Controller_6_3();
- $global_styles_revisions_controller->register_routes();
-}
-add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' );
-
-/**
- * Registers the Global Styles REST API routes.
- */
-function gutenberg_register_global_styles_endpoints() {
- $global_styles_controller = new Gutenberg_REST_Global_Styles_Controller_6_3();
- $global_styles_controller->register_routes();
-}
-add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' );
-
/**
* Add the `modified` value to the `wp_template` schema.
*
diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-controller-6-4.php
new file mode 100644
index 00000000000000..7ad1f264b383dc
--- /dev/null
+++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-controller-6-4.php
@@ -0,0 +1,269 @@
+schema ) {
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => $this->post_type,
+ 'type' => 'object',
+ 'properties' => array(
+ 'id' => array(
+ 'description' => __( 'ID of global styles config.', 'default' ),
+ 'type' => 'string',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'readonly' => true,
+ ),
+ 'styles' => array(
+ 'description' => __( 'Global styles.', 'default' ),
+ 'type' => array( 'object' ),
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'settings' => array(
+ 'description' => __( 'Global settings.', 'default' ),
+ 'type' => array( 'object' ),
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'behaviors' => array(
+ 'description' => __( 'Global behaviors.', 'default' ),
+ 'type' => array( 'object' ),
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'title' => array(
+ 'description' => __( 'Title of the global styles variation.', 'default' ),
+ 'type' => array( 'object', 'string' ),
+ 'default' => '',
+ 'context' => array( 'embed', 'view', 'edit' ),
+ 'properties' => array(
+ 'raw' => array(
+ 'description' => __( 'Title for the global styles variation, as it exists in the database.', 'default' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'rendered' => array(
+ 'description' => __( 'HTML title for the post, transformed for display.', 'default' ),
+ 'type' => 'string',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ 'readonly' => true,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $this->schema = $schema;
+
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ /**
+ * Prepare a global styles config output for response.
+ *
+ * @since 5.9.0
+ * @since 6.2 Handling of style.css was added to WP_Theme_JSON.
+ *
+ * @param WP_Post $post Global Styles post object.
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response Response object.
+ */
+ public function prepare_item_for_response( $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
+ $raw_config = json_decode( $post->post_content, true );
+ $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
+ $config = array();
+ if ( $is_global_styles_user_theme_json ) {
+ $config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data();
+ }
+
+ // Base fields for every post.
+ $data = array();
+ $fields = $this->get_fields_for_response( $request );
+
+ if ( rest_is_field_included( 'id', $fields ) ) {
+ $data['id'] = $post->ID;
+ }
+
+ if ( rest_is_field_included( 'title', $fields ) ) {
+ $data['title'] = array();
+ }
+ if ( rest_is_field_included( 'title.raw', $fields ) ) {
+ $data['title']['raw'] = $post->post_title;
+ }
+ if ( rest_is_field_included( 'title.rendered', $fields ) ) {
+ add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+
+ $data['title']['rendered'] = get_the_title( $post->ID );
+
+ remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
+ }
+
+ if ( rest_is_field_included( 'settings', $fields ) ) {
+ $data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass();
+ }
+
+ if ( rest_is_field_included( 'styles', $fields ) ) {
+ $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
+ }
+
+ if ( rest_is_field_included( 'behaviors', $fields ) ) {
+ $data['behaviors'] = ! empty( $config['behaviors'] ) && $is_global_styles_user_theme_json ? $config['behaviors'] : new stdClass();
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ // Wrap the data in a response object.
+ $response = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
+ $links = $this->prepare_links( $post->ID );
+ $response->add_links( $links );
+ if ( ! empty( $links['self']['href'] ) ) {
+ $actions = $this->get_available_actions();
+ $self = $links['self']['href'];
+ foreach ( $actions as $rel ) {
+ $response->add_link( $rel, $self );
+ }
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Returns the given theme global styles config.
+ * Duplicated from core.
+ * The only change is that we call WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' ) instead of WP_Theme_JSON_Resolver::get_merged_data( 'theme' ).
+ *
+ * @since 6.2.0
+ *
+ * @param WP_REST_Request $request The request instance.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function get_theme_item( $request ) {
+ if ( get_stylesheet() !== $request['stylesheet'] ) {
+ // This endpoint only supports the active theme for now.
+ return new WP_Error(
+ 'rest_theme_not_found',
+ __( 'Theme not found.', 'default' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $theme = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' );
+ $data = array();
+ $fields = $this->get_fields_for_response( $request );
+
+ if ( rest_is_field_included( 'settings', $fields ) ) {
+ $data['settings'] = $theme->get_settings();
+ }
+
+ if ( rest_is_field_included( 'styles', $fields ) ) {
+ $raw_data = $theme->get_raw_data();
+ $data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array();
+ }
+
+ if ( rest_is_field_included( 'behaviors', $fields ) ) {
+ $raw_data = $theme->get_raw_data();
+ $data['behaviors'] = isset( $raw_data['behaviors'] ) ? $raw_data['behaviors'] : array();
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ $response = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
+ $links = array(
+ 'self' => array(
+ 'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
+ ),
+ );
+ $response->add_links( $links );
+ }
+
+ return $response;
+ }
+
+ /**
+ * Prepares a single global styles config for update.
+ *
+ * @since 5.9.0
+ * @since 6.2.0 Added validation of styles.css property.
+ *
+ * @param WP_REST_Request $request Request object.
+ * @return stdClass Changes to pass to wp_update_post.
+ */
+ protected function prepare_item_for_database( $request ) {
+ $changes = new stdClass();
+ $changes->ID = $request['id'];
+ $post = get_post( $request['id'] );
+ $existing_config = array();
+ if ( $post ) {
+ $existing_config = json_decode( $post->post_content, true );
+ $json_decoding_error = json_last_error();
+ if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) ||
+ ! $existing_config['isGlobalStylesUserThemeJSON'] ) {
+ $existing_config = array();
+ }
+ }
+ if ( isset( $request['styles'] ) || isset( $request['settings'] ) || isset( $request['behaviors'] ) ) {
+ $config = array();
+ if ( isset( $request['styles'] ) ) {
+ $config['styles'] = $request['styles'];
+ if ( isset( $request['styles']['css'] ) ) {
+ $validate_custom_css = $this->validate_custom_css( $request['styles']['css'] );
+ if ( is_wp_error( $validate_custom_css ) ) {
+ return $validate_custom_css;
+ }
+ }
+ } elseif ( isset( $existing_config['styles'] ) ) {
+ $config['styles'] = $existing_config['styles'];
+ }
+ if ( isset( $request['settings'] ) ) {
+ $config['settings'] = $request['settings'];
+ } elseif ( isset( $existing_config['settings'] ) ) {
+ $config['settings'] = $existing_config['settings'];
+ }
+ if ( isset( $request['behaviors'] ) ) {
+ $config['behaviors'] = $request['behaviors'];
+ } elseif ( isset( $existing_config['behaviors'] ) ) {
+ $config['behaviors'] = $existing_config['behaviors'];
+ }
+ $config['isGlobalStylesUserThemeJSON'] = true;
+ $config['version'] = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA;
+ $changes->post_content = wp_json_encode( $config );
+ }
+ // Post title.
+ if ( isset( $request['title'] ) ) {
+ if ( is_string( $request['title'] ) ) {
+ $changes->post_title = $request['title'];
+ } elseif ( ! empty( $request['title']['raw'] ) ) {
+ $changes->post_title = $request['title']['raw'];
+ }
+ }
+ return $changes;
+ }
+
+}
diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php
new file mode 100644
index 00000000000000..42120b44bdcb62
--- /dev/null
+++ b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php
@@ -0,0 +1,172 @@
+get_parent( $request['parent'] );
+ $global_styles_config = $this->get_decoded_global_styles_json( $post->post_content );
+
+ if ( is_wp_error( $global_styles_config ) ) {
+ return $global_styles_config;
+ }
+
+ $fields = $this->get_fields_for_response( $request );
+ $data = array();
+
+ if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) || ! empty( $global_styles_config['behaviors'] ) ) {
+ $global_styles_config = ( new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' ) )->get_raw_data();
+ if ( rest_is_field_included( 'settings', $fields ) ) {
+ $data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass();
+ }
+ if ( rest_is_field_included( 'styles', $fields ) ) {
+ $data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass();
+ }
+ if ( rest_is_field_included( 'behaviors', $fields ) ) {
+ $data['behaviors'] = ! empty( $global_styles_config['behaviors'] ) ? $global_styles_config['behaviors'] : new stdClass();
+ }
+ }
+
+ if ( rest_is_field_included( 'author', $fields ) ) {
+ $data['author'] = (int) $post->post_author;
+ }
+
+ if ( rest_is_field_included( 'date', $fields ) ) {
+ $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
+ }
+
+ if ( rest_is_field_included( 'date_gmt', $fields ) ) {
+ $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
+ }
+
+ if ( rest_is_field_included( 'id', $fields ) ) {
+ $data['id'] = (int) $post->ID;
+ }
+
+ if ( rest_is_field_included( 'modified', $fields ) ) {
+ $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
+ }
+
+ if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
+ $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
+ }
+
+ if ( rest_is_field_included( 'parent', $fields ) ) {
+ $data['parent'] = (int) $parent->ID;
+ }
+
+ $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+ $data = $this->add_additional_fields_to_object( $data, $request );
+ $data = $this->filter_response_by_context( $data, $context );
+
+ return rest_ensure_response( $data );
+ }
+
+ /**
+ * Retrieves the revision's schema, conforming to JSON Schema.
+ *
+ * @since 6.3.0
+ *
+ * @return array Item schema data.
+ */
+ public function get_item_schema() {
+ if ( $this->schema ) {
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+
+ $schema = array(
+ '$schema' => 'http://json-schema.org/draft-04/schema#',
+ 'title' => "{$this->parent_post_type}-revision",
+ 'type' => 'object',
+ // Base properties for every Revision.
+ 'properties' => array(
+
+ /*
+ * Adds settings and styles from the WP_REST_Revisions_Controller item fields.
+ * Leaves out GUID as global styles shouldn't be accessible via URL.
+ */
+ 'author' => array(
+ 'description' => __( 'The ID for the author of the revision.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'date' => array(
+ 'description' => __( "The date the revision was published, in the site's timezone.", 'gutenberg' ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'date_gmt' => array(
+ 'description' => __( 'The date the revision was published, as GMT.', 'gutenberg' ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'id' => array(
+ 'description' => __( 'Unique identifier for the revision.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ 'modified' => array(
+ 'description' => __( "The date the revision was last modified, in the site's timezone.", 'gutenberg' ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'modified_gmt' => array(
+ 'description' => __( 'The date the revision was last modified, as GMT.', 'gutenberg' ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'parent' => array(
+ 'description' => __( 'The ID for the parent of the revision.', 'gutenberg' ),
+ 'type' => 'integer',
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+
+ // Adds settings and styles from the WP_REST_Global_Styles_Controller parent schema.
+ 'styles' => array(
+ 'description' => __( 'Global styles.', 'gutenberg' ),
+ 'type' => array( 'object' ),
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'settings' => array(
+ 'description' => __( 'Global settings.', 'gutenberg' ),
+ 'type' => array( 'object' ),
+ 'context' => array( 'view', 'edit' ),
+ ),
+ 'behaviors' => array(
+ 'description' => __( 'Global behaviors.', 'gutenberg' ),
+ 'type' => array( 'object' ),
+ 'context' => array( 'view', 'edit' ),
+ ),
+ ),
+ );
+
+ $this->schema = $schema;
+
+ return $this->add_additional_fields_schema( $this->schema );
+ }
+}
diff --git a/lib/compat/wordpress-6.4/rest-api.php b/lib/compat/wordpress-6.4/rest-api.php
new file mode 100644
index 00000000000000..53979f832c09a6
--- /dev/null
+++ b/lib/compat/wordpress-6.4/rest-api.php
@@ -0,0 +1,29 @@
+register_routes();
+}
+add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' );
+
+/**
+ * Registers the Global Styles REST API routes.
+ */
+function gutenberg_register_global_styles_endpoints() {
+ $global_styles_controller = new Gutenberg_REST_Global_Styles_Controller_6_4();
+ $global_styles_controller->register_routes();
+}
+add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' );
diff --git a/lib/experimental/behaviors.php b/lib/experimental/behaviors.php
deleted file mode 100644
index 62e7be7a252d49..00000000000000
--- a/lib/experimental/behaviors.php
+++ /dev/null
@@ -1,20 +0,0 @@
-get_data();
- if ( array_key_exists( 'behaviors', $theme_data ) ) {
- $settings['behaviors'] = $theme_data['behaviors'];
- }
- return $settings;
- },
- PHP_INT_MAX
-);
diff --git a/lib/load.php b/lib/load.php
index eeb463f7b762dc..3ffd026a8a444a 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -59,12 +59,16 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php';
require_once __DIR__ . '/compat/wordpress-6.3/footnotes.php';
+ // WordPress 6.4 compat.
+ require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-controller-6-4.php';
+ require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php';
+ require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php';
+
// Experimental.
if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) {
require_once __DIR__ . '/experimental/class-wp-rest-customizer-nonces.php';
}
require_once __DIR__ . '/experimental/class-gutenberg-rest-template-revision-count.php';
-
require_once __DIR__ . '/experimental/rest-api.php';
}
@@ -103,7 +107,6 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/compat/wordpress-6.3/kses.php';
// Experimental features.
-require __DIR__ . '/experimental/behaviors.php';
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
require __DIR__ . '/experimental/blocks.php';
require __DIR__ . '/experimental/navigation-theme-opt-in.php';
diff --git a/packages/block-editor/src/components/global-styles/behaviors-panel.js b/packages/block-editor/src/components/global-styles/behaviors-panel.js
new file mode 100644
index 00000000000000..fa8c2305ae0374
--- /dev/null
+++ b/packages/block-editor/src/components/global-styles/behaviors-panel.js
@@ -0,0 +1,71 @@
+/**
+ * WordPress dependencies
+ */
+import { SelectControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+export default function ( { onChange, value, behaviors } ) {
+ const defaultBehaviors = {
+ default: {
+ value: 'default',
+ label: __( 'Default' ),
+ },
+ noBehaviors: {
+ value: '',
+ label: __( 'No behaviors' ),
+ },
+ };
+
+ const behaviorsOptions = Object.entries( behaviors ).map(
+ ( [ behaviorName ] ) => ( {
+ value: behaviorName,
+ // Capitalize the first letter of the behavior name.
+ label: `${ behaviorName.charAt( 0 ).toUpperCase() }${ behaviorName
+ .slice( 1 )
+ .toLowerCase() }`,
+ } )
+ );
+
+ const options = [
+ ...Object.values( defaultBehaviors ),
+ ...behaviorsOptions,
+ ];
+
+ const animations = [
+ {
+ value: 'zoom',
+ label: __( 'Zoom' ),
+ },
+ {
+ value: 'fade',
+ label: __( 'Fade' ),
+ },
+ ];
+ return (
+
+
+ { value === 'lightbox' && (
+
+ ) }
+
+ );
+}
diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js
index 3c8b0167279804..96be5779124d74 100644
--- a/packages/block-editor/src/components/global-styles/hooks.js
+++ b/packages/block-editor/src/components/global-styles/hooks.js
@@ -8,7 +8,7 @@ import fastDeepEqual from 'fast-deep-equal/es6';
*/
import { useContext, useCallback, useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
-import { store as blocksStore } from '@wordpress/blocks';
+import { store as blocksStore, hasBlockSupport } from '@wordpress/blocks';
import { _x } from '@wordpress/i18n';
/**
@@ -19,10 +19,11 @@ import { getValueFromObjectPath, setImmutably } from '../../utils/object';
import { GlobalStylesContext } from './context';
import { unlock } from '../../lock-unlock';
-const EMPTY_CONFIG = { settings: {}, styles: {} };
+const EMPTY_CONFIG = { settings: {}, styles: {}, behaviors: {} };
const VALID_SETTINGS = [
'appearanceTools',
+ 'behaviors',
'useRootPaddingAwareAlignments',
'border.color',
'border.radius',
@@ -88,7 +89,6 @@ export const useGlobalStylesReset = () => {
export function useGlobalSetting( propertyPath, blockName, source = 'all' ) {
const { setUserConfig, ...configs } = useContext( GlobalStylesContext );
-
const appendedBlockPath = blockName ? '.blocks.' + blockName : '';
const appendedPropertyPath = propertyPath ? '.' + propertyPath : '';
const contextualPath = `settings${ appendedBlockPath }${ appendedPropertyPath }`;
@@ -135,7 +135,6 @@ export function useGlobalSetting( propertyPath, blockName, source = 'all' ) {
setImmutably( currentConfig, contextualPath.split( '.' ), newValue )
);
};
-
return [ settingValue, setSetting ];
}
@@ -461,3 +460,112 @@ export function useGradientsPerOrigin( settings ) {
shouldDisplayDefaultGradients,
] );
}
+
+export function __experimentalUseGlobalBehaviors( blockName, source = 'all' ) {
+ const {
+ merged: mergedConfig,
+ base: baseConfig,
+ user: userConfig,
+ setUserConfig,
+ } = useContext( GlobalStylesContext );
+ const finalPath = ! blockName
+ ? `behaviors`
+ : `behaviors.blocks.${ blockName }`;
+
+ let rawResult, result;
+ switch ( source ) {
+ case 'all':
+ rawResult = getValueFromObjectPath( mergedConfig, finalPath );
+ result = getValueFromVariable( mergedConfig, blockName, rawResult );
+ break;
+ case 'user':
+ rawResult = getValueFromObjectPath( userConfig, finalPath );
+ result = getValueFromVariable( mergedConfig, blockName, rawResult );
+ break;
+ case 'base':
+ rawResult = getValueFromObjectPath( baseConfig, finalPath );
+ result = getValueFromVariable( baseConfig, blockName, rawResult );
+ break;
+ default:
+ throw 'Unsupported source';
+ }
+
+ const animation = result?.lightbox?.animation || 'zoom';
+
+ const setBehavior = ( newValue ) => {
+ let newBehavior;
+ // The user saves with Apply Globally option.
+ if ( typeof newValue === 'object' ) {
+ newBehavior = newValue;
+ } else {
+ switch ( newValue ) {
+ case 'lightbox':
+ newBehavior = {
+ lightbox: {
+ enabled: true,
+ animation,
+ },
+ };
+ break;
+ case 'fade':
+ newBehavior = {
+ lightbox: {
+ enabled: true,
+ animation: 'fade',
+ },
+ };
+ break;
+ case 'zoom':
+ newBehavior = {
+ lightbox: {
+ enabled: true,
+ animation: 'zoom',
+ },
+ };
+ break;
+ case '':
+ newBehavior = {
+ lightbox: {
+ enabled: false,
+ animation,
+ },
+ };
+ break;
+ default:
+ break;
+ }
+ }
+ setUserConfig( ( currentConfig ) =>
+ setImmutably( currentConfig, finalPath.split( '.' ), newBehavior )
+ );
+ };
+ let behavior = '';
+ if ( result === undefined ) behavior = 'default';
+ if ( result?.lightbox.enabled ) behavior = 'lightbox';
+
+ return { behavior, inheritedBehaviors: result, setBehavior };
+}
+
+export function __experimentalUseHasBehaviorsPanel(
+ settings,
+ name,
+ { blockSupportOnly = false } = {}
+) {
+ if ( ! settings?.behaviors || ! window?.__experimentalInteractivityAPI ) {
+ return false;
+ }
+
+ // If every behavior is disabled on block supports, do not show the behaviors inspector control.
+ const hasSomeBlockSupport = Object.keys( settings?.behaviors ).some(
+ ( key ) => hasBlockSupport( name, `behaviors.${ key }` )
+ );
+
+ if ( blockSupportOnly ) {
+ return hasSomeBlockSupport;
+ }
+
+ // If every behavior is disabled, do not show the behaviors inspector control.
+ return Object.values( settings?.behaviors ).some(
+ ( value ) => value === true && hasSomeBlockSupport
+ );
+}
diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js
index 24bab543b9ada6..ee5c66ebe8a65b 100644
--- a/packages/block-editor/src/components/global-styles/index.js
+++ b/packages/block-editor/src/components/global-styles/index.js
@@ -1,4 +1,6 @@
export {
+ __experimentalUseGlobalBehaviors,
+ __experimentalUseHasBehaviorsPanel,
useGlobalStylesReset,
useGlobalSetting,
useGlobalStyle,
@@ -23,5 +25,6 @@ export { default as BorderPanel, useHasBorderPanel } from './border-panel';
export { default as ColorPanel, useHasColorPanel } from './color-panel';
export { default as EffectsPanel, useHasEffectsPanel } from './effects-panel';
export { default as FiltersPanel, useHasFiltersPanel } from './filters-panel';
+export { default as __experimentalBehaviorsPanel } from './behaviors-panel';
export { default as AdvancedPanel } from './advanced-panel';
export { areGlobalStyleConfigsEqual } from './utils';
diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js
index d4f2d959a33659..8f9e1f1f396001 100644
--- a/packages/block-editor/src/components/global-styles/utils.js
+++ b/packages/block-editor/src/components/global-styles/utils.js
@@ -415,6 +415,7 @@ export function areGlobalStyleConfigsEqual( original, variation ) {
}
return (
fastDeepEqual( original?.styles, variation?.styles ) &&
- fastDeepEqual( original?.settings, variation?.settings )
+ fastDeepEqual( original?.settings, variation?.settings ) &&
+ fastDeepEqual( original?.behaviors, variation?.behaviors )
);
}
diff --git a/packages/block-editor/src/hooks/behaviors.js b/packages/block-editor/src/hooks/behaviors.js
index 42cb42e8023684..27ef5b7afbfe93 100644
--- a/packages/block-editor/src/hooks/behaviors.js
+++ b/packages/block-editor/src/hooks/behaviors.js
@@ -22,15 +22,13 @@ function BehaviorsControl( {
onChangeAnimation,
disabled = false,
} ) {
- const { settings, themeBehaviors } = useSelect(
+ const { settings } = useSelect(
( select ) => {
- const { getBehaviors, getSettings } = select( blockEditorStore );
-
+ const { getSettings } = select( blockEditorStore );
return {
settings:
getSettings()?.__experimentalFeatures?.blocks?.[ blockName ]
- ?.behaviors,
- themeBehaviors: getBehaviors()?.blocks?.[ blockName ],
+ ?.behaviors || {},
};
},
[ blockName ]
@@ -46,7 +44,6 @@ function BehaviorsControl( {
label: __( 'No behaviors' ),
},
};
-
const behaviorsOptions = Object.entries( settings )
.filter(
( [ behaviorName, behaviorValue ] ) =>
@@ -60,7 +57,6 @@ function BehaviorsControl( {
.slice( 1 )
.toLowerCase() }`,
} ) );
-
const options = [
...Object.values( defaultBehaviors ),
...behaviorsOptions,
@@ -68,7 +64,6 @@ function BehaviorsControl( {
const { behaviors, behaviorsValue } = useMemo( () => {
const mergedBehaviors = {
- ...themeBehaviors,
...( blockBehaviors || {} ),
};
@@ -83,7 +78,8 @@ function BehaviorsControl( {
behaviors: mergedBehaviors,
behaviorsValue: value,
};
- }, [ blockBehaviors, themeBehaviors ] );
+ }, [ blockBehaviors ] );
+
// If every behavior is disabled, do not show the behaviors inspector control.
if ( behaviorsOptions.length === 0 ) {
return null;
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index 3aaec39a986ccd..c3d8847b032397 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -2577,30 +2577,6 @@ export function getSettings( state ) {
return state.settings;
}
-/**
- * Returns the behaviors registered with the editor.
- *
- * Behaviors are named, reusable pieces of functionality that can be
- * attached to blocks. They are registered with the editor using the
- * `theme.json` file.
- *
- * @example
- *
- * ```js
- * const behaviors = select( blockEditorStore ).getBehaviors();
- * if ( behaviors?.lightbox ) {
- * // Do something with the lightbox.
- * }
- *```
- *
- * @param {Object} state Editor state.
- *
- * @return {Object} The editor behaviors object.
- */
-export function getBehaviors( state ) {
- return state.settings.behaviors;
-}
-
/**
* Returns true if the most recent block change is be considered persistent, or
* false otherwise. A persistent change is one committed by BlockEditorProvider
diff --git a/packages/edit-site/src/components/global-styles/global-styles-provider.js b/packages/edit-site/src/components/global-styles/global-styles-provider.js
index 1e2d43e267a2dd..250cca0ebfc6df 100644
--- a/packages/edit-site/src/components/global-styles/global-styles-provider.js
+++ b/packages/edit-site/src/components/global-styles/global-styles-provider.js
@@ -31,7 +31,7 @@ export function mergeBaseAndUserConfigs( base, user ) {
}
function useGlobalStylesUserConfig() {
- const { globalStylesId, isReady, settings, styles } = useSelect(
+ const { globalStylesId, isReady, settings, styles, behaviors } = useSelect(
( select ) => {
const { getEditedEntityRecord, hasFinishedResolution } =
select( coreStore );
@@ -65,6 +65,7 @@ function useGlobalStylesUserConfig() {
isReady: hasResolved,
settings: record?.settings,
styles: record?.styles,
+ behaviors: record?.behaviors,
};
},
[]
@@ -76,8 +77,9 @@ function useGlobalStylesUserConfig() {
return {
settings: settings ?? {},
styles: styles ?? {},
+ behaviors: behaviors ?? {},
};
- }, [ settings, styles ] );
+ }, [ settings, styles, behaviors ] );
const setConfig = useCallback(
( callback, options = {} ) => {
@@ -89,6 +91,7 @@ function useGlobalStylesUserConfig() {
const currentConfig = {
styles: record?.styles ?? {},
settings: record?.settings ?? {},
+ behaviors: record?.behaviors ?? {},
};
const updatedConfig = callback( currentConfig );
editEntityRecord(
@@ -98,6 +101,8 @@ function useGlobalStylesUserConfig() {
{
styles: cleanEmptyObject( updatedConfig.styles ) || {},
settings: cleanEmptyObject( updatedConfig.settings ) || {},
+ behaviors:
+ cleanEmptyObject( updatedConfig.behaviors ) || {},
},
options
);
diff --git a/packages/edit-site/src/components/global-styles/screen-block.js b/packages/edit-site/src/components/global-styles/screen-block.js
index b24fd5eb41de15..ea3b5b89d116f1 100644
--- a/packages/edit-site/src/components/global-styles/screen-block.js
+++ b/packages/edit-site/src/components/global-styles/screen-block.js
@@ -61,12 +61,15 @@ const {
useHasDimensionsPanel,
useHasTypographyPanel,
useHasBorderPanel,
+ __experimentalUseHasBehaviorsPanel: useHasBehaviorsPanel,
useGlobalSetting,
useSettingsForBlockElement,
useHasColorPanel,
useHasEffectsPanel,
useHasFiltersPanel,
useGlobalStyle,
+ __experimentalUseGlobalBehaviors: useGlobalBehaviors,
+ __experimentalBehaviorsPanel: StylesBehaviorsPanel,
BorderPanel: StylesBorderPanel,
ColorPanel: StylesColorPanel,
TypographyPanel: StylesTypographyPanel,
@@ -91,10 +94,14 @@ function ScreenBlock( { name, variation } ) {
} );
const [ rawSettings, setSettings ] = useGlobalSetting( '', name );
const settings = useSettingsForBlockElement( rawSettings, name );
+ const { inheritedBehaviors, setBehavior } = useGlobalBehaviors( name );
+ const { behavior } = useGlobalBehaviors( name, 'user' );
+
const blockType = getBlockType( name );
const blockVariations = useBlockVariations( name );
const hasTypographyPanel = useHasTypographyPanel( settings );
const hasColorPanel = useHasColorPanel( settings );
+ const hasBehaviorsPanel = useHasBehaviorsPanel( rawSettings, name );
const hasBorderPanel = useHasBorderPanel( settings );
const hasDimensionsPanel = useHasDimensionsPanel( settings );
const hasEffectsPanel = useHasEffectsPanel( settings );
@@ -267,6 +274,14 @@ function ScreenBlock( { name, variation } ) {
onChange={ setStyle }
inheritedValue={ inheritedStyle }
/>
+ { hasBehaviorsPanel && (
+
+ ) }
) }
>
diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js
index c6920f3d63c24f..b21c14418cbeef 100644
--- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js
+++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js
@@ -70,6 +70,7 @@ function ScreenRevisions() {
setUserConfig( () => ( {
styles: revision?.styles,
settings: revision?.settings,
+ behaviors: revision?.behaviors,
} ) );
setIsLoadingRevisionWithUnsavedChanges( false );
onCloseRevisions();
@@ -79,6 +80,7 @@ function ScreenRevisions() {
setGlobalStylesRevision( {
styles: revision?.styles,
settings: revision?.settings,
+ behaviors: revision?.behaviors,
id: revision?.id,
} );
setSelectedRevisionId( revision?.id );
diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js
index ce3123e3fd0285..5aee31f1ff99a2 100644
--- a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js
+++ b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js
@@ -82,6 +82,7 @@ export default function useGlobalStylesRevisions() {
id: 'unsaved',
styles: userConfig?.styles,
settings: userConfig?.settings,
+ behaviors: userConfig?.behaviors,
author: {
name: currentUser?.name,
avatar_urls: currentUser?.avatar_urls,
diff --git a/packages/edit-site/src/components/global-styles/style-variations-container.js b/packages/edit-site/src/components/global-styles/style-variations-container.js
index 6cc8b53b800d3a..69a66a707d2528 100644
--- a/packages/edit-site/src/components/global-styles/style-variations-container.js
+++ b/packages/edit-site/src/components/global-styles/style-variations-container.js
@@ -113,11 +113,13 @@ export default function StyleVariationsContainer() {
title: __( 'Default' ),
settings: {},
styles: {},
+ behaviors: {},
},
...( variations ?? [] ).map( ( variation ) => ( {
...variation,
settings: variation.settings ?? {},
styles: variation.styles ?? {},
+ behaviors: variation.behaviors ?? {},
} ) ),
];
}, [ variations ] );
diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
index 6e55b89d22b2cf..455f18ef74eba3 100644
--- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
+++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js
@@ -25,9 +25,12 @@ import { store as noticesStore } from '@wordpress/notices';
import { useSupportedStyles } from '../../components/global-styles/hooks';
import { unlock } from '../../lock-unlock';
-const { GlobalStylesContext, useBlockEditingMode } = unlock(
- blockEditorPrivateApis
-);
+const {
+ GlobalStylesContext,
+ useBlockEditingMode,
+ __experimentalUseGlobalBehaviors: useGlobalBehaviors,
+ __experimentalUseHasBehaviorsPanel: useHasBehaviorsPanel,
+} = unlock( blockEditorPrivateApis );
// TODO: Temporary duplication of constant in @wordpress/block-editor. Can be
// removed by moving PushChangesToGlobalStylesControl to
@@ -121,7 +124,7 @@ function useChangesToPush( name, attributes ) {
: getValueFromObjectPath( attributes.style, path );
return value ? [ { path, value } ] : [];
} ),
- [ supports, name, attributes ]
+ [ supports, attributes ]
);
}
@@ -176,6 +179,9 @@ function PushChangesToGlobalStylesControl( {
} ) {
const changes = useChangesToPush( name, attributes );
+ const hasBehaviorsPanel = useHasBehaviorsPanel( attributes, name, {
+ blockSupportOnly: true,
+ } );
const { user: userConfig, setUserConfig } =
useContext( GlobalStylesContext );
@@ -183,55 +189,86 @@ function PushChangesToGlobalStylesControl( {
useDispatch( blockEditorStore );
const { createSuccessNotice } = useDispatch( noticesStore );
+ const { inheritedBehaviors, setBehavior } = useGlobalBehaviors( name );
+
+ const userHasEditedBehaviors =
+ attributes.hasOwnProperty( 'behaviors' ) && hasBehaviorsPanel;
+
const pushChanges = useCallback( () => {
- if ( changes.length === 0 ) {
+ if ( changes.length === 0 && ! userHasEditedBehaviors ) {
return;
}
+ if ( changes.length > 0 ) {
+ const { style: blockStyles } = attributes;
- const { style: blockStyles } = attributes;
+ const newBlockStyles = cloneDeep( blockStyles );
+ const newUserConfig = cloneDeep( userConfig );
- const newBlockStyles = cloneDeep( blockStyles );
- const newUserConfig = cloneDeep( userConfig );
+ for ( const { path, value } of changes ) {
+ setNestedValue( newBlockStyles, path, undefined );
+ setNestedValue(
+ newUserConfig,
+ [ 'styles', 'blocks', name, ...path ],
+ value
+ );
+ }
- for ( const { path, value } of changes ) {
- setNestedValue( newBlockStyles, path, undefined );
- setNestedValue(
- newUserConfig,
- [ 'styles', 'blocks', name, ...path ],
- value
+ // @wordpress/core-data doesn't support editing multiple entity types in
+ // a single undo level. So for now, we disable @wordpress/core-data undo
+ // tracking and implement our own Undo button in the snackbar
+ // notification.
+ __unstableMarkNextChangeAsNotPersistent();
+ setAttributes( { style: newBlockStyles } );
+ setUserConfig( () => newUserConfig, { undoIgnore: true } );
+ createSuccessNotice(
+ sprintf(
+ // translators: %s: Title of the block e.g. 'Heading'.
+ __( '%s styles applied.' ),
+ getBlockType( name ).title
+ ),
+ {
+ type: 'snackbar',
+ actions: [
+ {
+ label: __( 'Undo' ),
+ onClick() {
+ __unstableMarkNextChangeAsNotPersistent();
+ setAttributes( { style: blockStyles } );
+ setUserConfig( () => userConfig, {
+ undoIgnore: true,
+ } );
+ },
+ },
+ ],
+ }
);
}
-
- // @wordpress/core-data doesn't support editing multiple entity types in
- // a single undo level. So for now, we disable @wordpress/core-data undo
- // tracking and implement our own Undo button in the snackbar
- // notification.
- __unstableMarkNextChangeAsNotPersistent();
- setAttributes( { style: newBlockStyles } );
- setUserConfig( () => newUserConfig, { undoIgnore: true } );
-
- createSuccessNotice(
- sprintf(
- // translators: %s: Title of the block e.g. 'Heading'.
- __( '%s styles applied.' ),
- getBlockType( name ).title
- ),
- {
- type: 'snackbar',
- actions: [
- {
- label: __( 'Undo' ),
- onClick() {
- __unstableMarkNextChangeAsNotPersistent();
- setAttributes( { style: blockStyles } );
- setUserConfig( () => userConfig, {
- undoIgnore: true,
- } );
+ if ( userHasEditedBehaviors ) {
+ __unstableMarkNextChangeAsNotPersistent();
+ setBehavior( attributes.behaviors );
+ createSuccessNotice(
+ sprintf(
+ // translators: %s: Title of the block e.g. 'Heading'.
+ __( '%s behaviors applied.' ),
+ getBlockType( name ).title
+ ),
+ {
+ type: 'snackbar',
+ actions: [
+ {
+ label: __( 'Undo' ),
+ onClick() {
+ __unstableMarkNextChangeAsNotPersistent();
+ setBehavior( inheritedBehaviors );
+ setUserConfig( () => userConfig, {
+ undoIgnore: true,
+ } );
+ },
},
- },
- ],
- }
- );
+ ],
+ }
+ );
+ }
}, [ changes, attributes, userConfig, name ] );
return (
@@ -240,7 +277,7 @@ function PushChangesToGlobalStylesControl( {
help={ sprintf(
// translators: %s: Title of the block e.g. 'Heading'.
__(
- 'Apply this block’s typography, spacing, dimensions, and color styles to all %s blocks.'
+ 'Apply this block’s typography, spacing, dimensions, color styles, and behaviors to all %s blocks.'
),
getBlockType( name ).title
) }
@@ -250,7 +287,7 @@ function PushChangesToGlobalStylesControl( {