diff --git a/docs/designers-developers/developers/block-api/block-deprecation.md b/docs/designers-developers/developers/block-api/block-deprecation.md index ba1dd2f25b1f97..d598a157d4ab71 100644 --- a/docs/designers-developers/developers/block-api/block-deprecation.md +++ b/docs/designers-developers/developers/block-api/block-deprecation.md @@ -1,19 +1,47 @@ # Deprecated Blocks -When updating static blocks markup and attributes, block authors need to consider existing posts using the old versions of their block. In order to provide a good upgrade path, you can choose one of the following strategies: +When updating static blocks markup and attributes, block authors need to consider existing posts using the old versions of their block. To provide a good upgrade path, you can choose one of the following strategies: - Do not deprecate the block and create a new one (a different name) - Provide a "deprecated" version of the block allowing users opening these in the block editor to edit them using the updated block. -A block can have several deprecated versions. A deprecation will be tried if a parsed block appears to be invalid, or if there is a deprecation defined for which its `isEligible` property function returns true. +A block can have several deprecated versions. A deprecation will be tried if the current state of a parsed block is invalid, or if the deprecation defines an `isEligible` function that returns true. + +It is important to note that if a deprecation's `save` method does not produce a valid block then it is skipped, including its `migrate` method, even if `isEligible` would return true for the given attributes. This means that if you have several deprecations for a block and want to perform a new migration, like moving content to `InnerBlocks`, you may need to include the `migrate` method in multiple deprecations for it to be applied to all previous versions of the block. + +Deprecations do not operate as a chain of updates in the way other software data updates, like database migrations, do. At first glance, it is easy to think that each deprecation is going to make the required changes to the data and then hand this new form of the block onto the next deprecation to make its changes. What happens instead, is that each deprecation is passed the original saved content, and if its `save` method produces valid content the deprecation is used to parse the block attributes. If it has a `migrate` method it will also be run using the attributes parsed by the deprecation. The current block is updated with the migrated attributes and inner blocks before the current block's `save` function is run to generate new valid content for the block. At this point the current block should now be in a valid state. + +For blocks with multiple deprecations, it may be easier to save each deprecation to a constant with the version of the block it applies to, and then add each of these to the block's `deprecated` array. The deprecations in the array should be in reverse chronological order. This allows the block editor to attempt to apply the most recent and likely deprecations first, avoiding unnecessary and expensive processing. + +### Example: + +{% codetabs %} +{% ESNext %} +```js +const v1 = {}; +const v2 = {}; +const v3 = {}; +const deprecated = [ v3, v2, v1 ]; + +``` +{% ES5 %} +```js +var v1 = {}; +var v2 = {}; +var v3 = {}; +var deprecated = [ v3, v2, v1 ]; +``` +{% end %} + +It is also recommended to keep [fixtures](https://github.com/WordPress/gutenberg/blob/master/packages/e2e-tests/fixtures/blocks/README.md) which contain the different versions of the block content to allow you to easily test that new deprecations and migrations are working across all previous versions of the block. Deprecations are defined on a block type as its `deprecated` property, an array of deprecation objects where each object takes the form: - `attributes` (Object): The [attributes definition](/docs/designers-developers/developers/block-api/block-attributes.md) of the deprecated form of the block. - `supports` (Object): The [supports definition](/docs/designers-developers/developers/block-api/block-registration.md) of the deprecated form of the block. - `save` (Function): The [save implementation](/docs/designers-developers/developers/block-api/block-edit-save.md) of the deprecated form of the block. -- `migrate` (Function, Optional): A function which, given the old attributes and inner blocks is expected to return either the new attributes or a tuple array of `[ attributes, innerBlocks ]` compatible with the block. -- `isEligible` (Function, Optional): A function which, given the attributes and inner blocks of the parsed block, returns true if the deprecation can handle the block migration even if the block is valid. This function is not called when the block is invalid. This is particularly useful in cases where a block is technically valid even once deprecated, and requires updates to its attributes or inner blocks. +- `migrate` (Function, Optional): A function which, given the old attributes and inner blocks is expected to return either the new attributes or a tuple array of `[ attributes, innerBlocks ]` compatible with the block. As mentioned above, a deprecation's `migrate` will not be run if its `save` function does not return a valid block so you will need to make sure your migrations are available in all the deprecations where they are relevant. +- `isEligible` (Function, Optional): A function which, given the attributes and inner blocks of the parsed block, returns true if the deprecation can handle the block migration even if the block is valid. This is particularly useful in cases where a block is technically valid even once deprecated, and requires updates to its attributes or inner blocks. This function is not called when the results of all previous deprecations' `save` functions were invalid. It's important to note that `attributes`, `supports`, and `save` are not automatically inherited from the current version, since they can impact parsing and serialization of a block, so they must be defined on the deprecated object in order to be processed during a migration. diff --git a/docs/designers-developers/developers/data/data-core-edit-post.md b/docs/designers-developers/developers/data/data-core-edit-post.md index 85e5d162c0bf97..03be0d1e9ff917 100644 --- a/docs/designers-developers/developers/data/data-core-edit-post.md +++ b/docs/designers-developers/developers/data/data-core-edit-post.md @@ -372,10 +372,6 @@ _Returns_ Returns an action object used to request meta box update. -_Returns_ - -- `Object`: Action object. - # **setAvailableMetaBoxesPerLocation** Returns an action object used in signaling @@ -385,10 +381,6 @@ _Parameters_ - _metaBoxesPerLocation_ `Object`: Meta boxes per location. -_Returns_ - -- `Object`: Action object. - # **setIsInserterOpened** Returns an action object used to open/close the inserter. diff --git a/docs/designers-developers/developers/slotfills/main-dashboard-button.md b/docs/designers-developers/developers/slotfills/main-dashboard-button.md index 43b3730a35bcf4..3392eed131218f 100644 --- a/docs/designers-developers/developers/slotfills/main-dashboard-button.md +++ b/docs/designers-developers/developers/slotfills/main-dashboard-button.md @@ -1,18 +1,19 @@ # MainDashboardButton -This slot allows replacing the default main dashboard button in the post editor -that's used for closing the editor in fullscreen mode. In the site editor this slot -refers to the "back to dashboard" button in the navigation sidebar. +This slot allows replacing the default main dashboard button in the post editor and site editor. +It's used for returning back to main wp-admin dashboard when editor is in fullscreen mode. ## Examples -Basic usage: +### Post editor example + +This will override the W icon button in the header. ```js import { registerPlugin } from '@wordpress/plugins'; import { __experimentalMainDashboardButton as MainDashboardButton, -} from '@wordpress/interface'; +} from '@wordpress/edit-post'; const MainDashboardButtonTest = () => ( @@ -32,16 +33,14 @@ the post editor, that can be achieved in the following way: import { registerPlugin } from '@wordpress/plugins'; import { __experimentalFullscreenModeClose as FullscreenModeClose, -} from '@wordpress/edit-post'; -import { __experimentalMainDashboardButton as MainDashboardButton, -} from '@wordpress/interface'; +} from '@wordpress/edit-post'; import { close } from '@wordpress/icons'; const MainDashboardButtonIconTest = () => ( - + ); @@ -50,13 +49,15 @@ registerPlugin( 'main-dashboard-button-icon-test', { } ); ``` -Site editor example: +### Site editor example + +In the site editor this slot refers to the "back to dashboard" button in the navigation sidebar. ```js import { registerPlugin } from '@wordpress/plugins'; import { __experimentalMainDashboardButton as MainDashboardButton, -} from '@wordpress/interface'; +} from '@wordpress/edit-site'; import { __experimentalNavigationBackButton as NavigationBackButton, } from '@wordpress/components'; diff --git a/docs/designers-developers/developers/themes/theme-json.md b/docs/designers-developers/developers/themes/theme-json.md index 86fbfc115716a9..35918fc1ec343c 100644 --- a/docs/designers-developers/developers/themes/theme-json.md +++ b/docs/designers-developers/developers/themes/theme-json.md @@ -97,6 +97,7 @@ The settings section has the following structure and default values: "dropCap": true, /* false to opt-out */ "fontFamilies": [ ... ], /* font family presets */ "fontSizes": [ ... ], /* font size presets, as in add_theme_support('editor-font-sizes', ... ) */ + "fontStyles": [ ... ], /* font style presets */ "fontWeights": [ ... ], /* font weight presets */ "textDecorations": [ ... ], /* text decoration presets */ "textTransforms": [ ... ] /* text transform presets */ @@ -349,6 +350,7 @@ These are the current typography properties supported by blocks: | Context | Font Family | Font Size | Font Style | Font Weight | Line Height | Text Decoration | Text Transform | | --- | --- | --- | --- | --- | --- | --- | --- | | Global | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Code | - | Yes | - | - | - | - | - | | Heading [1] | - | Yes | - | - | Yes | - | - | | List | - | Yes | - | - | - | - | - | | Navigation | Yes | Yes | Yes | Yes | - | Yes | Yes | diff --git a/docs/designers-developers/developers/tutorials/block-based-themes/README.md b/docs/designers-developers/developers/tutorials/block-based-themes/README.md index 1b13d6214f2331..a701cd46228814 100644 --- a/docs/designers-developers/developers/tutorials/block-based-themes/README.md +++ b/docs/designers-developers/developers/tutorials/block-based-themes/README.md @@ -15,7 +15,7 @@ This tutorial is up to date as of Gutenberg version 9.1. 1. [What is needed to create a block-based theme?](/docs/designers-developers/developers/tutorials/block-based-themes/README.md#what-is-needed-to-create-a-block-based-theme) 2. [Creating the theme](/docs/designers-developers/developers/tutorials/block-based-themes/README.md#creating-the-theme) 3. [Creating the templates and template parts](/docs/designers-developers/developers/tutorials/block-based-themes/README.md#creating-the-templates-and-template-parts) - 4. [Experimental-theme.json -Global styles](/docs/designers-developers/developers/tutorials/block-based-themes/README.md#experimental-theme-json-global-styles) + 4. [Experimental-theme.json - Global styles](/docs/designers-developers/developers/tutorials/block-based-themes/README.md#experimental-theme-json-global-styles) 5. [Adding blocks](/docs/designers-developers/developers/tutorials/block-based-themes/block-based-themes-2-adding-blocks.md) ## What is needed to create a block-based theme? @@ -34,7 +34,7 @@ A block based theme requires an `index.php` file, an index template file, a `sty The theme may optionally include an [experimental-theme.json file](/docs/designers-developers/developers/themes/theme-json.md) to manage global styles. You decide what additional templates and template parts to include in your theme. -Templates are placed inside the block-templates folder, and template parts are placed inside the block-template-parts folder: +Templates are placed inside the `block-templates` folder, and template parts are placed inside the `block-template-parts` folder: ``` theme @@ -57,7 +57,7 @@ theme ## Creating the theme Create a new folder for your theme in `/wp-content/themes/`. -Inside this folder, create the block-templates and block-template-parts folders. +Inside this folder, create the `block-templates` and `block-template-parts` folders. Create a `style.css` file. The file header in the `style.css` file has [the same items that you would use in a traditional theme](https://developer.wordpress.org/themes/basics/main-stylesheet-style-css/#explanations). @@ -166,7 +166,7 @@ theme ### Creating the templates and template parts -Create two template parts called `footer.html` and `header.html` and place them inside the block-template-parts folder. You can leave the files empty for now. +Create two template parts called `footer.html` and `header.html` and place them inside the `block-template-parts` folder. You can leave the files empty for now. Inside the block-templates folder, create an `index.html` file. @@ -318,7 +318,7 @@ Below are the presets and styles combined: ``` { "global": { - "setttings": { + "settings": { "color": { "palette": [ { diff --git a/docs/manifest.json b/docs/manifest.json index dee68b8b1e8d36..5f856ce44bef88 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -191,6 +191,12 @@ "markdown_source": "../docs/designers-developers/developers/slotfills/README.md", "parent": "developers" }, + { + "title": "MainDashboardButton", + "slug": "main-dashboard-button", + "markdown_source": "../docs/designers-developers/developers/slotfills/main-dashboard-button.md", + "parent": "slotfills" + }, { "title": "PluginBlockSettingsMenuItem", "slug": "plugin-block-settings-menu-item", diff --git a/docs/toc.json b/docs/toc.json index ff54916e323e32..08ef2cb73f2a25 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -35,6 +35,7 @@ { "docs/designers-developers/developers/filters/autocomplete-filters.md": [] } ] }, {"docs/designers-developers/developers/slotfills/README.md": [ + { "docs/designers-developers/developers/slotfills/main-dashboard-button.md": [] }, { "docs/designers-developers/developers/slotfills/plugin-block-settings-menu-item.md": [] }, { "docs/designers-developers/developers/slotfills/plugin-document-setting-panel.md": [] }, { "docs/designers-developers/developers/slotfills/plugin-more-menu-item.md": [] }, diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index d797bdc21bac41..29454c43296fd1 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -15,12 +15,19 @@ function gutenberg_register_typography_support( $block_type ) { return; } - $has_font_appearance_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontAppearance' ), false ); $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); + $has_font_style_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontStyle' ), false ); + $has_font_weight_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontWeight' ), false ); $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); $has_text_decoration_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextDecoration' ), false ); $has_text_transform_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextTransform' ), false ); - $has_typography_support = $has_font_appearance_support || $has_font_size_support || $has_line_height_support || $has_text_transform_support || $has_text_decoration_support; + + $has_typography_support = $has_font_size_support + || $has_font_weight_support + || $has_font_style_support + || $has_line_height_support + || $has_text_transform_support + || $has_text_decoration_support; if ( ! $block_type->attributes ) { $block_type->attributes = array(); @@ -57,8 +64,9 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { $classes = array(); $styles = array(); - $has_font_appearance_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontAppearance' ), false ); $has_font_family_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontFamily' ), false ); + $has_font_style_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontStyle' ), false ); + $has_font_weight_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontWeight' ), false ); $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); $has_text_decoration_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextDecoration' ), false ); @@ -94,14 +102,17 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { } } - // Font appearance - style and weight. - if ( $has_font_appearance_support ) { + // Font style. + if ( $has_font_style_support ) { // Apply font style. $font_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontStyle', 'font-style' ); if ( $font_style ) { $styles[] = $font_style; } + } + // Font weight. + if ( $has_font_weight_support ) { // Apply font weight. $font_weight = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontWeight', 'font-weight' ); if ( $font_weight ) { diff --git a/lib/class-wp-theme-json-resolver.php b/lib/class-wp-theme-json-resolver.php new file mode 100644 index 00000000000000..76a7507f275362 --- /dev/null +++ b/lib/class-wp-theme-json-resolver.php @@ -0,0 +1,383 @@ + __( 'Black', 'gutenberg' ), + 'cyan-bluish-gray' => __( 'Cyan bluish gray', 'gutenberg' ), + 'white' => __( 'White', 'gutenberg' ), + 'pale-pink' => __( 'Pale pink', 'gutenberg' ), + 'vivid-red' => __( 'Vivid red', 'gutenberg' ), + 'luminous-vivid-orange' => __( 'Luminous vivid orange', 'gutenberg' ), + 'luminous-vivid-amber' => __( 'Luminous vivid amber', 'gutenberg' ), + 'light-green-cyan' => __( 'Light green cyan', 'gutenberg' ), + 'vivid-green-cyan' => __( 'Vivid green cyan', 'gutenberg' ), + 'pale-cyan-blue' => __( 'Pale cyan blue', 'gutenberg' ), + 'vivid-cyan-blue' => __( 'Vivid cyan blue', 'gutenberg' ), + 'vivid-purple' => __( 'Vivid purple', 'gutenberg' ), + ); + if ( ! empty( $config['global']['settings']['color']['palette'] ) ) { + foreach ( $config['global']['settings']['color']['palette'] as &$color ) { + $color['name'] = $default_colors_i18n[ $color['slug'] ]; + } + } + + $default_gradients_i18n = array( + 'vivid-cyan-blue-to-vivid-purple' => __( 'Vivid cyan blue to vivid purple', 'gutenberg' ), + 'light-green-cyan-to-vivid-green-cyan' => __( 'Light green cyan to vivid green cyan', 'gutenberg' ), + 'luminous-vivid-amber-to-luminous-vivid-orange' => __( 'Luminous vivid amber to luminous vivid orange', 'gutenberg' ), + 'luminous-vivid-orange-to-vivid-red' => __( 'Luminous vivid orange to vivid red', 'gutenberg' ), + 'very-light-gray-to-cyan-bluish-gray' => __( 'Very light gray to cyan bluish gray', 'gutenberg' ), + 'cool-to-warm-spectrum' => __( 'Cool to warm spectrum', 'gutenberg' ), + 'blush-light-purple' => __( 'Blush light purple', 'gutenberg' ), + 'blush-bordeaux' => __( 'Blush bordeaux', 'gutenberg' ), + 'luminous-dusk' => __( 'Luminous dusk', 'gutenberg' ), + 'pale-ocean' => __( 'Pale ocean', 'gutenberg' ), + 'electric-grass' => __( 'Electric grass', 'gutenberg' ), + 'midnight' => __( 'Midnight', 'gutenberg' ), + ); + if ( ! empty( $config['global']['settings']['color']['gradients'] ) ) { + foreach ( $config['global']['settings']['color']['gradients'] as &$gradient ) { + $gradient['name'] = $default_gradients_i18n[ $gradient['slug'] ]; + } + } + + $default_font_sizes_i18n = array( + 'small' => __( 'Small', 'gutenberg' ), + 'normal' => __( 'Normal', 'gutenberg' ), + 'medium' => __( 'Medium', 'gutenberg' ), + 'large' => __( 'Large', 'gutenberg' ), + 'huge' => __( 'Huge', 'gutenberg' ), + ); + if ( ! empty( $config['global']['settings']['typography']['fontSizes'] ) ) { + foreach ( $config['global']['settings']['typography']['fontSizes'] as &$font_size ) { + $font_size['name'] = $default_font_sizes_i18n[ $font_size['slug'] ]; + } + } + + $default_font_styles_i18n = array( + 'normal' => __( 'Regular', 'gutenberg' ), + 'italic' => __( 'Italic', 'gutenberg' ), + 'initial' => __( 'Initial', 'gutenberg' ), + 'inherit' => __( 'Inherit', 'gutenberg' ), + ); + if ( ! empty( $config['global']['settings']['typography']['fontStyles'] ) ) { + foreach ( $config['global']['settings']['typography']['fontStyles'] as &$font_style ) { + $font_style['name'] = $default_font_styles_i18n[ $font_style['slug'] ]; + } + } + + $default_font_weights_i18n = array( + '100' => __( 'Ultralight', 'gutenberg' ), + '200' => __( 'Thin', 'gutenberg' ), + '300' => __( 'Light', 'gutenberg' ), + '400' => __( 'Regular', 'gutenberg' ), + '500' => __( 'Medium', 'gutenberg' ), + '600' => __( 'Semibold', 'gutenberg' ), + '700' => __( 'Bold', 'gutenberg' ), + '800' => __( 'Heavy', 'gutenberg' ), + '900' => __( 'Black', 'gutenberg' ), + 'initial' => __( 'Initial', 'gutenberg' ), + 'inherit' => __( 'Inherit', 'gutenberg' ), + ); + if ( ! empty( $config['global']['settings']['typography']['fontWeights'] ) ) { + foreach ( $config['global']['settings']['typography']['fontWeights'] as &$font_weight ) { + $font_weight['name'] = $default_font_weights_i18n[ $font_weight['slug'] ]; + } + } + // End i18n logic to remove when JSON i18 strings are extracted. + + self::$core = new WP_Theme_JSON( $config ); + + return self::$core; + } + + /** + * Returns the theme's origin config. + * + * It uses the theme support data if + * the theme hasn't declared any via theme.json. + * + * @param array $theme_support_data Theme support data in theme.json format. + * + * @return WP_Theme_JSON Entity that holds theme data. + */ + private function get_theme_origin( $theme_support_data = array() ) { + $theme_json_data = self::get_from_file( locate_template( 'experimental-theme.json' ) ); + + /* + * We want the presets and settings declared in theme.json + * to override the ones declared via add_theme_support. + */ + $this->theme = new WP_Theme_JSON( $theme_support_data ); + $this->theme->merge( new WP_Theme_JSON( $theme_json_data ) ); + + return $this->theme; + } + + /** + * Returns the CPT that contains the user's origin config + * for the current theme or a void array if none found. + * + * It can also create and return a new draft CPT. + * + * @param bool $should_create_cpt Whether a new CPT should be created if no one was found. + * False by default. + * @param array $post_status_filter Filter CPT by post status. + * ['publish'] by default, so it only fetches published posts. + * + * @return array Custom Post Type for the user's origin config. + */ + private static function get_user_data_from_custom_post_type( $should_create_cpt = false, $post_status_filter = array( 'publish' ) ) { + $user_cpt = array(); + $post_type_filter = 'wp_global_styles'; + $post_name_filter = 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ); + $recent_posts = wp_get_recent_posts( + array( + 'numberposts' => 1, + 'orderby' => 'date', + 'order' => 'desc', + 'post_type' => $post_type_filter, + 'post_status' => $post_status_filter, + 'name' => $post_name_filter, + ) + ); + + if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) { + $user_cpt = $recent_posts[0]; + } elseif ( $should_create_cpt ) { + $cpt_post_id = wp_insert_post( + array( + 'post_content' => '{}', + 'post_status' => 'publish', + 'post_type' => $post_type_filter, + 'post_name' => $post_name_filter, + ), + true + ); + $user_cpt = get_post( $cpt_post_id, ARRAY_A ); + } + + return $user_cpt; + } + + /** + * Returns the user's origin config. + * + * @return WP_Theme_JSON Entity that holds user data. + */ + private static function get_user_origin() { + if ( null !== self::$user ) { + return self::$user; + } + + $config = array(); + $user_cpt = self::get_user_data_from_custom_post_type(); + if ( array_key_exists( 'post_content', $user_cpt ) ) { + $decoded_data = json_decode( $user_cpt['post_content'], true ); + + $json_decoding_error = json_last_error(); + if ( JSON_ERROR_NONE !== $json_decoding_error ) { + error_log( 'Error when decoding user schema: ' . json_last_error_msg() ); + return $config; + } + + if ( is_array( $decoded_data ) ) { + $config = $decoded_data; + } + } + self::$user = new WP_Theme_JSON( $config ); + + return self::$user; + } + + /** + * There are three sources of data for a site: + * core, theme, and user. + * + * The main function of the resolver is to + * merge all this data following this algorithm: + * theme overrides core, and user overrides + * data coming from either theme or core. + * + * user data > theme data > core data + * + * The main use case for the resolver is to return + * the merged data up to the user level.However, + * there are situations in which we need the + * data merged up to a different level (theme) + * or no merged at all. + * + * @param array $theme_support_data Existing block editor settings. + * Empty array by default. + * @param string $origin The source of data the consumer wants. + * Valid values are 'core', 'theme', 'user'. + * Default is 'user'. + * @param boolean $merged Whether the data should be merged + * with the previous origins (the default). + * + * @return WP_Theme_JSON + */ + public function get_origin( $theme_support_data = array(), $origin = 'user', $merged = true ) { + + if ( ( 'user' === $origin ) && $merged ) { + $result = new WP_Theme_JSON(); + $result->merge( self::get_core_origin() ); + $result->merge( $this->get_theme_origin( $theme_support_data ) ); + $result->merge( self::get_user_origin() ); + return $result; + } + + if ( ( 'theme' === $origin ) && $merged ) { + $result = new WP_Theme_JSON(); + $result->merge( self::get_core_origin() ); + $result->merge( $this->get_theme_origin( $theme_support_data ) ); + return $result; + } + + if ( 'user' === $origin ) { + return self::get_user_origin(); + } + + if ( 'theme' === $origin ) { + return $this->get_theme_origin( $theme_support_data ); + } + + return self::get_core_origin(); + } + + /** + * Registers a Custom Post Type to store the user's origin config. + */ + public static function register_user_custom_post_type() { + if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { + return; + } + + $args = array( + 'label' => __( 'Global Styles', 'gutenberg' ), + 'description' => 'CPT to store user design tokens', + 'public' => false, + 'show_ui' => false, + 'show_in_rest' => true, + 'rest_base' => '__experimental/global-styles', + 'capabilities' => array( + 'read' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'edit_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + 'edit_others_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + ), + 'map_meta_cap' => true, + 'supports' => array( + 'editor', + 'revisions', + ), + ); + register_post_type( 'wp_global_styles', $args ); + } + + /** + * Returns the ID of the custom post type + * that stores user data. + * + * @return integer + */ + public static function get_user_custom_post_type_id() { + if ( null !== self::$user_custom_post_type_id ) { + return self::$user_custom_post_type_id; + } + + $user_cpt = self::get_user_data_from_custom_post_type( true ); + if ( array_key_exists( 'ID', $user_cpt ) ) { + self::$user_custom_post_type_id = $user_cpt['ID']; + } + + return self::$user_custom_post_type_id; + } + +} diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php index 0452510d306c6c..ea66bc5283a98a 100644 --- a/lib/class-wp-theme-json.php +++ b/lib/class-wp-theme-json.php @@ -221,7 +221,7 @@ class WP_Theme_JSON { ), array( 'path' => array( 'settings', 'typography', 'fontStyles' ), - 'value_key' => 'slug', + 'value_key' => 'value', 'css_var_infix' => 'font-style', 'classes' => array( array( @@ -232,7 +232,7 @@ class WP_Theme_JSON { ), array( 'path' => array( 'settings', 'typography', 'fontWeights' ), - 'value_key' => 'slug', + 'value_key' => 'value', 'css_var_infix' => 'font-weight', 'classes' => array( array( @@ -300,11 +300,11 @@ class WP_Theme_JSON { ), 'fontStyle' => array( 'value' => array( 'typography', 'fontStyle' ), - 'support' => array( '__experimentalFontAppearance' ), + 'support' => array( '__experimentalFontStyle' ), ), 'fontWeight' => array( 'value' => array( 'typography', 'fontWeight' ), - 'support' => array( '__experimentalFontAppearance' ), + 'support' => array( '__experimentalFontWeight' ), ), 'lineHeight' => array( 'value' => array( 'typography', 'lineHeight' ), @@ -790,6 +790,9 @@ private static function compute_theme_vars( &$declarations, $context ) { * @return string CSS ruleset. */ private static function to_ruleset( $selector, $declarations ) { + if ( empty( $declarations ) ) { + return ''; + } $ruleset = ''; if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { @@ -816,17 +819,49 @@ function ( $carry, $element ) { /** * Converts each context into a list of rulesets * to be appended to the stylesheet. + * These rulesets contain all the css variables (custom variables and preset variables). * * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax * * For each context this creates a new ruleset such as: * * context-selector { - * style-property-one: value; * --wp--preset--category--slug: value; * --wp--custom--variable: value; * } * + * @param string $stylesheet Stylesheet to append new rules to. + * @param array $context Context to be processed. + * + * @return string The new stylesheet. + */ + private static function to_css_variables( $stylesheet, $context ) { + if ( empty( $context['selector'] ) ) { + return $stylesheet; + } + + $declarations = array(); + self::compute_preset_vars( $declarations, $context ); + self::compute_theme_vars( $declarations, $context ); + + // Attach the ruleset for style and custom properties. + $stylesheet .= self::to_ruleset( $context['selector'], $declarations ); + + return $stylesheet; + } + + /** + * Converts each context into a list of rulesets + * containing the block styles to be appended to the stylesheet. + * + * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax + * + * For each context this creates a new ruleset such as: + * + * context-selector { + * style-property-one: value; + * } + * * Additionally, it'll also create new rulesets * as classes for each preset value such as: * @@ -846,29 +881,23 @@ function ( $carry, $element ) { * background: value; * } * + * p.has-value-gradient-background { + * background: value; + * } + * * @param string $stylesheet Stylesheet to append new rules to. * @param array $context Context to be processed. * * @return string The new stylesheet. */ - private static function to_stylesheet( $stylesheet, $context ) { + private static function to_block_styles( $stylesheet, $context ) { if ( empty( $context['selector'] ) ) { - return ''; + return $stylesheet; } $declarations = array(); self::compute_style_properties( $declarations, $context ); - self::compute_preset_vars( $declarations, $context ); - self::compute_theme_vars( $declarations, $context ); - // If there are no declarations at this point, - // it won't have any preset classes either, - // so bail out earlier. - if ( empty( $declarations ) ) { - return ''; - } - - // Attach the ruleset for style and custom properties. $stylesheet .= self::to_ruleset( $context['selector'], $declarations ); // Attach the rulesets for the classes. @@ -910,10 +939,18 @@ function ( $element ) { * Returns the stylesheet that results of processing * the theme.json structure this object represents. * + * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'. * @return string Stylesheet. */ - public function get_stylesheet() { - return array_reduce( $this->contexts, array( $this, 'to_stylesheet' ), '' ); + public function get_stylesheet( $type = 'all' ) { + switch ( $type ) { + case 'block_styles': + return array_reduce( $this->contexts, array( $this, 'to_block_styles' ), '' ); + case 'css_variables': + return array_reduce( $this->contexts, array( $this, 'to_css_variables' ), '' ); + default: + return array_reduce( $this->contexts, array( $this, 'to_css_variables' ), '' ) . array_reduce( $this->contexts, array( $this, 'to_block_styles' ), '' ); + } } /** diff --git a/lib/edit-site-page.php b/lib/edit-site-page.php index 891a39bf47178b..46c5995d67b805 100644 --- a/lib/edit-site-page.php +++ b/lib/edit-site-page.php @@ -182,7 +182,7 @@ function gutenberg_edit_site_init( $hook ) { */ function register_site_editor_homepage_settings() { register_setting( - 'general', + 'reading', 'show_on_front', array( 'show_in_rest' => true, @@ -192,7 +192,7 @@ function register_site_editor_homepage_settings() { ); register_setting( - 'general', + 'reading', 'page_on_front', array( 'show_in_rest' => true, diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json index 7b35275d9af036..82c1bccf28e738 100644 --- a/lib/experimental-default-theme.json +++ b/lib/experimental-default-theme.json @@ -164,49 +164,60 @@ "fontStyles": [ { "name": "Regular", - "slug": "normal" + "slug": "normal", + "value": "normal" }, { "name": "Italic", - "slug": "italic" + "slug": "italic", + "value": "italic" } ], "fontWeights": [ { "name": "Ultralight", - "slug": "100" + "slug": "100", + "value": "100" }, { "name": "Thin", - "slug": "200" + "slug": "200", + "value": "200" }, { "name": "Light", - "slug": "300" + "slug": "300", + "value": "300" }, { "name": "Regular", - "slug": "400" + "slug": "400", + "value": "400" }, { "name": "Medium", - "slug": "500" + "slug": "500", + "value": "500" }, { "name": "Semibold", - "slug": "600" + "slug": "600", + "value": "600" }, { "name": "Bold", - "slug": "700" + "slug": "700", + "value": "700" }, { "name": "Heavy", - "slug": "800" + "slug": "800", + "value": "800" }, { "name": "Black", - "slug": "900" + "slug": "900", + "value": "900" } ], "textTransforms": [ diff --git a/lib/full-site-editing/default-template-types.php b/lib/full-site-editing/default-template-types.php index df70c1bd15865d..54cc722be96f37 100644 --- a/lib/full-site-editing/default-template-types.php +++ b/lib/full-site-editing/default-template-types.php @@ -14,68 +14,72 @@ function gutenberg_get_default_template_types() { $default_template_types = array( 'index' => array( - 'title' => _x( 'Default (Index)', 'Template name', 'gutenberg' ), - 'description' => __( 'Main template, applied when no other template is found', 'gutenberg' ), + 'title' => _x( 'Index', 'Template name', 'gutenberg' ), + 'description' => __( 'The default template which is used when no other template can be found', 'gutenberg' ), ), 'home' => array( 'title' => _x( 'Home', 'Template name', 'gutenberg' ), - 'description' => __( 'Template for the latest blog posts', 'gutenberg' ), + 'description' => __( 'The home page template, which is the front page by default. If you use a static front page this is the template for the page with the latest posts', 'gutenberg' ), ), 'front-page' => array( 'title' => _x( 'Front Page', 'Template name', 'gutenberg' ), - 'description' => __( 'Front page template, whether it displays the blog posts index or a static page', 'gutenberg' ), + 'description' => __( 'Used when the site home page is queried', 'gutenberg' ), ), 'singular' => array( - 'title' => _x( 'Default Singular', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays any content on a single page', 'gutenberg' ), + 'title' => _x( 'Singular', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a single entry is queried. This template will be overridden the Single, Post, and Page templates where appropriate', 'gutenberg' ), ), 'single' => array( - 'title' => _x( 'Default Single', 'Template name', 'gutenberg' ), - 'description' => __( 'Applied to individual content like a blog post', 'gutenberg' ), + 'title' => _x( 'Single', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a single entry that is not a Page is queried', 'gutenberg' ), + ), + 'single-post' => array( + 'title' => _x( 'Post', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a single Post is queried', 'gutenberg' ), ), 'page' => array( - 'title' => _x( 'Default Page', 'Template name', 'gutenberg' ), - 'description' => __( 'Applied to individual pages', 'gutenberg' ), + 'title' => _x( 'Page', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when an individual Page is queried', 'gutenberg' ), ), 'archive' => array( - 'title' => _x( 'Default Archive', 'Template name', 'gutenberg' ), - 'description' => __( 'Applied to archives like your posts page, categories, or tags', 'gutenberg' ), + 'title' => _x( 'Archive', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when multiple entries are queried. This template will be overridden the Category, Author, and Date templates where appropriate', 'gutenberg' ), ), 'author' => array( - 'title' => _x( 'Default Author Archive', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a list of posts by a single author', 'gutenberg' ), + 'title' => _x( 'Author Archive', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a list of Posts from a single author is queried', 'gutenberg' ), ), 'category' => array( - 'title' => _x( 'Default Post Category Archive', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a list of posts in a category', 'gutenberg' ), + 'title' => _x( 'Post Category Archive', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a list of Posts from a category is queried', 'gutenberg' ), ), 'taxonomy' => array( - 'title' => _x( 'Default Taxonomy Archive', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a list of posts in a taxonomy', 'gutenberg' ), + 'title' => _x( 'Taxonomy Archive', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a list of posts from a taxonomy is queried', 'gutenberg' ), ), 'date' => array( - 'title' => _x( 'Default Date Archive', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a list of posts in a date range', 'gutenberg' ), + 'title' => _x( 'Date Archive', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a list of Posts from a certain date are queried', 'gutenberg' ), ), 'tag' => array( - 'title' => _x( 'Default Tag Archive', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a list of posts with a tag', 'gutenberg' ), + 'title' => _x( 'Tag Archive', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a list of Posts with a certain tag is queried', 'gutenberg' ), ), 'attachment' => array( 'title' => __( 'Media', 'gutenberg' ), - 'description' => __( 'Displays media content', 'gutenberg' ), + 'description' => __( 'Used when a Media entry is queried', 'gutenberg' ), ), 'search' => array( - 'title' => __( 'Search Results', 'gutenberg' ), - 'description' => __( 'Applied to search results', 'gutenberg' ), + 'title' => _x( 'Search Results', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when a visitor searches the site', 'gutenberg' ), ), 'privacy-policy' => array( 'title' => __( 'Privacy Policy', 'gutenberg' ), 'description' => '', ), '404' => array( - 'title' => _x( '404 (Not Found)', 'Template name', 'gutenberg' ), - 'description' => __( 'Applied when content cannot be found', 'gutenberg' ), + 'title' => _x( '404', 'Template name', 'gutenberg' ), + 'description' => __( 'Used when the queried content cannot be found', 'gutenberg' ), ), ); diff --git a/lib/full-site-editing/templates-utils.php b/lib/full-site-editing/templates-utils.php index 62fb7f6da83bbc..5430470cfa640a 100644 --- a/lib/full-site-editing/templates-utils.php +++ b/lib/full-site-editing/templates-utils.php @@ -53,7 +53,10 @@ function gutenberg_render_templates_lists_custom_column( $column_name, $post_id } if ( 'theme' === $column_name ) { - $terms = get_the_terms( $post_id, 'wp_theme' ); + $terms = get_the_terms( $post_id, 'wp_theme' ); + if ( empty( $terms ) || is_wp_error( $terms ) ) { + return; + } $themes = array(); $is_file_based = false; foreach ( $terms as $term ) { @@ -77,7 +80,7 @@ function gutenberg_render_templates_lists_custom_column( $column_name, $post_id * @param array $views The edit views to filter. */ function gutenberg_filter_templates_edit_views( $views ) { - $post_type = get_current_screen()->post_type; + $post_type = get_current_screen()->post_type; $url = add_query_arg( array( 'post_type' => $post_type, diff --git a/lib/global-styles.php b/lib/global-styles.php index 9ec1d698da1ff9..6c62d867fc967d 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -14,218 +14,6 @@ function gutenberg_experimental_global_styles_has_theme_json_support() { return is_readable( locate_template( 'experimental-theme.json' ) ); } -/** - * Processes a file that adheres to the theme.json - * schema and returns an array with its contents, - * or a void array if none found. - * - * @param string $file_path Path to file. - * @return array Contents that adhere to the theme.json schema. - */ -function gutenberg_experimental_global_styles_get_from_file( $file_path ) { - $config = array(); - if ( file_exists( $file_path ) ) { - $decoded_file = json_decode( - file_get_contents( $file_path ), - true - ); - - $json_decoding_error = json_last_error(); - if ( JSON_ERROR_NONE !== $json_decoding_error ) { - error_log( 'Error when decoding file schema: ' . json_last_error_msg() ); - return $config; - } - - if ( is_array( $decoded_file ) ) { - $config = $decoded_file; - } - } - return $config; -} - -/** - * Returns the user's origin config. - * - * @return WP_Theme_JSON Entity that holds user data. - */ -function gutenberg_experimental_global_styles_get_user() { - $config = array(); - $user_cpt = gutenberg_experimental_global_styles_get_user_cpt(); - if ( array_key_exists( 'post_content', $user_cpt ) ) { - $decoded_data = json_decode( $user_cpt['post_content'], true ); - - $json_decoding_error = json_last_error(); - if ( JSON_ERROR_NONE !== $json_decoding_error ) { - error_log( 'Error when decoding user schema: ' . json_last_error_msg() ); - return $config; - } - - if ( is_array( $decoded_data ) ) { - $config = $decoded_data; - } - } - - return new WP_Theme_JSON( $config ); -} - -/** - * Returns the CPT that contains the user's origin config - * for the current theme or a void array if none found. - * - * It can also create and return a new draft CPT. - * - * @param bool $should_create_cpt Whether a new CPT should be created if no one was found. - * False by default. - * @param array $post_status_filter Filter CPT by post status. - * ['publish'] by default, so it only fetches published posts. - * @return array Custom Post Type for the user's origin config. - */ -function gutenberg_experimental_global_styles_get_user_cpt( $should_create_cpt = false, $post_status_filter = array( 'publish' ) ) { - $user_cpt = array(); - $post_type_filter = 'wp_global_styles'; - $post_name_filter = 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ); - $recent_posts = wp_get_recent_posts( - array( - 'numberposts' => 1, - 'orderby' => 'date', - 'order' => 'desc', - 'post_type' => $post_type_filter, - 'post_status' => $post_status_filter, - 'name' => $post_name_filter, - ) - ); - - if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) { - $user_cpt = $recent_posts[0]; - } elseif ( $should_create_cpt ) { - $cpt_post_id = wp_insert_post( - array( - 'post_content' => '{}', - 'post_status' => 'publish', - 'post_type' => $post_type_filter, - 'post_name' => $post_name_filter, - ), - true - ); - $user_cpt = get_post( $cpt_post_id, ARRAY_A ); - } - - return $user_cpt; -} - -/** - * Returns the post ID of the CPT containing the user's origin config. - * - * @return integer - */ -function gutenberg_experimental_global_styles_get_user_cpt_id() { - $user_cpt_id = null; - $user_cpt = gutenberg_experimental_global_styles_get_user_cpt( true ); - if ( array_key_exists( 'ID', $user_cpt ) ) { - $user_cpt_id = $user_cpt['ID']; - } - return $user_cpt_id; -} - -/** - * Return core's origin config. - * - * @return WP_Theme_JSON Entity that holds core data. - */ -function gutenberg_experimental_global_styles_get_core() { - $config = gutenberg_experimental_global_styles_get_from_file( - __DIR__ . '/experimental-default-theme.json' - ); - - // Start i18n logic to remove when JSON i18 strings are extracted. - $default_colors_i18n = array( - 'black' => __( 'Black', 'gutenberg' ), - 'cyan-bluish-gray' => __( 'Cyan bluish gray', 'gutenberg' ), - 'white' => __( 'White', 'gutenberg' ), - 'pale-pink' => __( 'Pale pink', 'gutenberg' ), - 'vivid-red' => __( 'Vivid red', 'gutenberg' ), - 'luminous-vivid-orange' => __( 'Luminous vivid orange', 'gutenberg' ), - 'luminous-vivid-amber' => __( 'Luminous vivid amber', 'gutenberg' ), - 'light-green-cyan' => __( 'Light green cyan', 'gutenberg' ), - 'vivid-green-cyan' => __( 'Vivid green cyan', 'gutenberg' ), - 'pale-cyan-blue' => __( 'Pale cyan blue', 'gutenberg' ), - 'vivid-cyan-blue' => __( 'Vivid cyan blue', 'gutenberg' ), - 'vivid-purple' => __( 'Vivid purple', 'gutenberg' ), - ); - if ( ! empty( $config['global']['settings']['color']['palette'] ) ) { - foreach ( $config['global']['settings']['color']['palette'] as &$color ) { - $color['name'] = $default_colors_i18n[ $color['slug'] ]; - } - } - - $default_gradients_i18n = array( - 'vivid-cyan-blue-to-vivid-purple' => __( 'Vivid cyan blue to vivid purple', 'gutenberg' ), - 'light-green-cyan-to-vivid-green-cyan' => __( 'Light green cyan to vivid green cyan', 'gutenberg' ), - 'luminous-vivid-amber-to-luminous-vivid-orange' => __( 'Luminous vivid amber to luminous vivid orange', 'gutenberg' ), - 'luminous-vivid-orange-to-vivid-red' => __( 'Luminous vivid orange to vivid red', 'gutenberg' ), - 'very-light-gray-to-cyan-bluish-gray' => __( 'Very light gray to cyan bluish gray', 'gutenberg' ), - 'cool-to-warm-spectrum' => __( 'Cool to warm spectrum', 'gutenberg' ), - 'blush-light-purple' => __( 'Blush light purple', 'gutenberg' ), - 'blush-bordeaux' => __( 'Blush bordeaux', 'gutenberg' ), - 'luminous-dusk' => __( 'Luminous dusk', 'gutenberg' ), - 'pale-ocean' => __( 'Pale ocean', 'gutenberg' ), - 'electric-grass' => __( 'Electric grass', 'gutenberg' ), - 'midnight' => __( 'Midnight', 'gutenberg' ), - ); - if ( ! empty( $config['global']['settings']['color']['gradients'] ) ) { - foreach ( $config['global']['settings']['color']['gradients'] as &$gradient ) { - $gradient['name'] = $default_gradients_i18n[ $gradient['slug'] ]; - } - } - - $default_font_sizes_i18n = array( - 'small' => __( 'Small', 'gutenberg' ), - 'normal' => __( 'Normal', 'gutenberg' ), - 'medium' => __( 'Medium', 'gutenberg' ), - 'large' => __( 'Large', 'gutenberg' ), - 'huge' => __( 'Huge', 'gutenberg' ), - ); - if ( ! empty( $config['global']['settings']['typography']['fontSizes'] ) ) { - foreach ( $config['global']['settings']['typography']['fontSizes'] as &$font_size ) { - $font_size['name'] = $default_font_sizes_i18n[ $font_size['slug'] ]; - } - } - - $default_font_styles_i18n = array( - 'normal' => __( 'Regular', 'gutenberg' ), - 'italic' => __( 'Italic', 'gutenberg' ), - 'initial' => __( 'Initial', 'gutenberg' ), - 'inherit' => __( 'Inherit', 'gutenberg' ), - ); - if ( ! empty( $config['global']['settings']['typography']['fontStyles'] ) ) { - foreach ( $config['global']['settings']['typography']['fontStyles'] as &$font_style ) { - $font_style['name'] = $default_font_styles_i18n[ $font_style['slug'] ]; - } - } - - $default_font_weights_i18n = array( - '100' => __( 'Ultralight', 'gutenberg' ), - '200' => __( 'Thin', 'gutenberg' ), - '300' => __( 'Light', 'gutenberg' ), - '400' => __( 'Regular', 'gutenberg' ), - '500' => __( 'Medium', 'gutenberg' ), - '600' => __( 'Semibold', 'gutenberg' ), - '700' => __( 'Bold', 'gutenberg' ), - '800' => __( 'Heavy', 'gutenberg' ), - '900' => __( 'Black', 'gutenberg' ), - 'initial' => __( 'Initial', 'gutenberg' ), - 'inherit' => __( 'Inherit', 'gutenberg' ), - ); - if ( ! empty( $config['global']['settings']['typography']['fontWeights'] ) ) { - foreach ( $config['global']['settings']['typography']['fontWeights'] as &$font_weight ) { - $font_weight['name'] = $default_font_weights_i18n[ $font_weight['slug'] ]; - } - } - // End i18n logic to remove when JSON i18 strings are extracted. - - return new WP_Theme_JSON( $config ); -} - /** * Returns the theme presets registered via add_theme_support, if any. * @@ -322,43 +110,19 @@ function gutenberg_experimental_global_styles_get_theme_support_settings( $setti return $theme_settings; } -/** - * Returns the theme's origin config. - * - * It also fetches the existing presets the theme declared via add_theme_support - * and uses them if the theme hasn't declared any via theme.json. - * - * @param array $settings Existing editor settings. - * - * @return WP_Theme_JSON Entity that holds theme data. - */ -function gutenberg_experimental_global_styles_get_theme( $settings ) { - $theme_support_data = gutenberg_experimental_global_styles_get_theme_support_settings( $settings ); - $theme_json_data = gutenberg_experimental_global_styles_get_from_file( - locate_template( 'experimental-theme.json' ) - ); - - /* - * We want the presets and settings declared in theme.json - * to override the ones declared via add_theme_support. - */ - $result = new WP_Theme_JSON( $theme_support_data ); - $result->merge( new WP_Theme_JSON( $theme_json_data ) ); - - return $result; -} - /** * Takes a tree adhering to the theme.json schema and generates * the corresponding stylesheet. * * @param WP_Theme_JSON $tree Input tree. + * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'. * * @return string Stylesheet. */ -function gutenberg_experimental_global_styles_get_stylesheet( $tree ) { +function gutenberg_experimental_global_styles_get_stylesheet( $tree, $type = 'all' ) { // Check if we can use cached. $can_use_cached = ( + ( 'all' === $type ) && ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) && ( ! defined( 'SCRIPT_DEBUG' ) || ! SCRIPT_DEBUG ) && ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) && @@ -373,9 +137,9 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree ) { } } - $stylesheet = $tree->get_stylesheet(); + $stylesheet = $tree->get_stylesheet( $type ); - if ( gutenberg_experimental_global_styles_has_theme_json_support() ) { + if ( ( 'all' === $type || 'block_styles' === $type ) && gutenberg_experimental_global_styles_has_theme_json_support() ) { // To support all themes, we added in the block-library stylesheet // a style rule such as .has-link-color a { color: var(--wp--style--color--link, #00e); } // so that existing link colors themes used didn't break. @@ -386,7 +150,7 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree ) { if ( $can_use_cached ) { // Cache for a minute. - // This cache doesn't need to be any longer, we only want to avoid spikes on high-trafic sites. + // This cache doesn't need to be any longer, we only want to avoid spikes on high-traffic sites. set_transient( 'global_styles', $stylesheet, MINUTE_IN_SECONDS ); } @@ -398,10 +162,11 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree ) { * and enqueues the resulting stylesheet. */ function gutenberg_experimental_global_styles_enqueue_assets() { - $settings = gutenberg_get_common_block_editor_settings(); - $all = gutenberg_experimental_global_styles_get_core(); - $all->merge( gutenberg_experimental_global_styles_get_theme( $settings ) ); - $all->merge( gutenberg_experimental_global_styles_get_user() ); + $settings = gutenberg_get_common_block_editor_settings(); + $theme_support_data = gutenberg_experimental_global_styles_get_theme_support_settings( $settings ); + + $resolver = new WP_Theme_JSON_Resolver(); + $all = $resolver->get_origin( $theme_support_data ); $stylesheet = gutenberg_experimental_global_styles_get_stylesheet( $all ); if ( empty( $stylesheet ) ) { @@ -420,20 +185,7 @@ function gutenberg_experimental_global_styles_enqueue_assets() { * @return array New block editor settings */ function gutenberg_experimental_global_styles_settings( $settings ) { - $base = gutenberg_experimental_global_styles_get_core(); - $all = gutenberg_experimental_global_styles_get_core(); - $theme = gutenberg_experimental_global_styles_get_theme( $settings ); - $user = gutenberg_experimental_global_styles_get_user(); - - $base->merge( $theme ); - - $all->merge( $theme ); - $all->merge( $user ); - - // STEP 1: ADD FEATURES - // These need to be added to settings always. - // We also need to unset the deprecated settings defined by core. - $settings['__experimentalFeatures'] = $all->get_settings(); + $theme_support_data = gutenberg_experimental_global_styles_get_theme_support_settings( $settings ); unset( $settings['colors'] ); unset( $settings['disableCustomColors'] ); unset( $settings['disableCustomFontSizes'] ); @@ -443,6 +195,14 @@ function gutenberg_experimental_global_styles_settings( $settings ) { unset( $settings['fontSizes'] ); unset( $settings['gradients'] ); + $resolver = new WP_Theme_JSON_Resolver(); + $all = $resolver->get_origin( $theme_support_data ); + $base = $resolver->get_origin( $theme_support_data, 'theme' ); + + // STEP 1: ADD FEATURES + // These need to be added to settings always. + $settings['__experimentalFeatures'] = $all->get_settings(); + // STEP 2 - IF EDIT-SITE, ADD DATA REQUIRED FOR GLOBAL STYLES SIDEBAR // The client needs some information to be able to access/update the user styles. // We only do this if the theme has support for theme.json, though, @@ -454,7 +214,7 @@ function_exists( 'gutenberg_is_edit_site_page' ) && gutenberg_is_edit_site_page( $screen->id ) && gutenberg_experimental_global_styles_has_theme_json_support() ) { - $settings['__experimentalGlobalStylesUserEntityId'] = gutenberg_experimental_global_styles_get_user_cpt_id(); + $settings['__experimentalGlobalStylesUserEntityId'] = WP_Theme_JSON_Resolver::get_user_custom_post_type_id(); $settings['__experimentalGlobalStylesBaseStyles'] = $base->get_raw_data(); } else { // STEP 3 - OTHERWISE, ADD STYLES @@ -463,45 +223,18 @@ function_exists( 'gutenberg_is_edit_site_page' ) && // we need to add the styles via the settings. This is because // we want them processed as if they were added via add_editor_styles, // which adds the editor wrapper class. - $settings['styles'][] = array( 'css' => gutenberg_experimental_global_styles_get_stylesheet( $all ) ); + $settings['styles'][] = array( + 'css' => gutenberg_experimental_global_styles_get_stylesheet( $all, 'css_variables' ), + '__experimentalNoWrapper' => true, + ); + $settings['styles'][] = array( + 'css' => gutenberg_experimental_global_styles_get_stylesheet( $all, 'block_styles' ), + ); } return $settings; } -/** - * Registers a Custom Post Type to store the user's origin config. - */ -function gutenberg_experimental_global_styles_register_cpt() { - if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { - return; - } - - $args = array( - 'label' => __( 'Global Styles', 'gutenberg' ), - 'description' => 'CPT to store user design tokens', - 'public' => false, - 'show_ui' => false, - 'show_in_rest' => true, - 'rest_base' => '__experimental/global-styles', - 'capabilities' => array( - 'read' => 'edit_theme_options', - 'create_posts' => 'edit_theme_options', - 'edit_posts' => 'edit_theme_options', - 'edit_published_posts' => 'edit_theme_options', - 'delete_published_posts' => 'edit_theme_options', - 'edit_others_posts' => 'edit_theme_options', - 'delete_others_posts' => 'edit_theme_options', - ), - 'map_meta_cap' => true, - 'supports' => array( - 'editor', - 'revisions', - ), - ); - register_post_type( 'wp_global_styles', $args ); -} - -add_action( 'init', 'gutenberg_experimental_global_styles_register_cpt' ); +add_action( 'init', array( 'WP_Theme_JSON_Resolver', 'register_user_custom_post_type' ) ); add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings', PHP_INT_MAX ); add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' ); diff --git a/lib/load.php b/lib/load.php index 9227918378a0fd..967c995ee413a8 100644 --- a/lib/load.php +++ b/lib/load.php @@ -124,6 +124,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/navigation-page.php'; require __DIR__ . '/experiments-page.php'; require __DIR__ . '/class-wp-theme-json.php'; +require __DIR__ . '/class-wp-theme-json-resolver.php'; require __DIR__ . '/global-styles.php'; if ( ! class_exists( 'WP_Block_Supports' ) ) { diff --git a/lib/patterns/heading-paragraph.php b/lib/patterns/heading-paragraph.php index 9a8b160a787727..d06caa1d8cc2c8 100644 --- a/lib/patterns/heading-paragraph.php +++ b/lib/patterns/heading-paragraph.php @@ -8,7 +8,6 @@ return array( 'title' => __( 'Heading and paragraph', 'gutenberg' ), 'content' => "\n
\n

2.
" . __( 'Which treats of the first sally the ingenious Don Quixote made from home', 'gutenberg' ) . "

\n\n\n\n

" . __( 'These preliminaries settled, he did not care to put off any longer the execution of his design, urged on to it by the thought of all the world was losing by his delay, seeing what wrongs he intended to right, grievances to redress, injustices to repair, abuses to remove, and duties to discharge.', 'gutenberg' ) . "

\n
\n", - 'viewportWidth' => 1000, 'categories' => array( 'text' ), 'description' => _x( 'A heading preceded by a chapter number, and followed by a paragraph.', 'Block pattern description', 'gutenberg' ), ); diff --git a/lib/patterns/large-header-button.php b/lib/patterns/large-header-button.php index 1d71e5703f5b5f..bed0ce49062d50 100644 --- a/lib/patterns/large-header-button.php +++ b/lib/patterns/large-header-button.php @@ -6,9 +6,8 @@ */ return array( - 'title' => __( 'Large header with a heading and a button ', 'gutenberg' ), - 'content' => "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

" . __( 'Thou hast seen', 'gutenberg' ) . '
' . __( 'nothing yet', 'gutenberg' ) . "

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n", - 'viewportWidth' => 1000, - 'categories' => array( 'header' ), - 'description' => _x( 'A large hero section with a bright gradient background, a big heading and a filled button.', 'Block pattern description', 'gutenberg' ), + 'title' => __( 'Large header with a heading and a button ', 'gutenberg' ), + 'content' => "\n
\n
\n
\n
\n
\n\n\n\n
\n
\n\n\n\n

" . __( 'Thou hast seen', 'gutenberg' ) . '
' . __( 'nothing yet', 'gutenberg' ) . "

\n\n\n\n\n\n\n\n
\n
\n\n\n\n
\n
\n
\n
\n
\n", + 'categories' => array( 'header' ), + 'description' => _x( 'A large hero section with a bright gradient background, a big heading and a filled button.', 'Block pattern description', 'gutenberg' ), ); diff --git a/lib/patterns/large-header.php b/lib/patterns/large-header.php index 0213bf8bfeb724..0583e1e6a03b2b 100644 --- a/lib/patterns/large-header.php +++ b/lib/patterns/large-header.php @@ -6,9 +6,8 @@ */ return array( - 'title' => __( 'Large header with a heading', 'gutenberg' ), - 'content' => "\n
\n

" . __( 'Don Quixote', 'gutenberg' ) . "

\n
\n", - 'viewportWidth' => 1000, - 'categories' => array( 'header' ), - 'description' => _x( 'A large hero section with an example background image and a heading in the center.', 'Block pattern description', 'gutenberg' ), + 'title' => __( 'Large header with a heading', 'gutenberg' ), + 'content' => "\n
\n

" . __( 'Don Quixote', 'gutenberg' ) . "

\n
\n", + 'categories' => array( 'header' ), + 'description' => _x( 'A large hero section with an example background image and a heading in the center.', 'Block pattern description', 'gutenberg' ), ); diff --git a/lib/patterns/text-three-columns-buttons.php b/lib/patterns/text-three-columns-buttons.php index 629c839b89bb88..8386be74b9c5bb 100644 --- a/lib/patterns/text-three-columns-buttons.php +++ b/lib/patterns/text-three-columns-buttons.php @@ -6,9 +6,8 @@ */ return array( - 'title' => __( 'Three columns of text with buttons', 'gutenberg' ), - 'categories' => array( 'columns' ), - 'content' => "\n
\n
\n
\n

" . __( 'Which treats of the character and pursuits of the famous Don Quixote of La Mancha.', 'gutenberg' ) . "

\n\n\n\n\n
\n\n\n\n
\n

" . __( 'Which treats of the first sally the ingenious Don Quixote made from home.', 'gutenberg' ) . "

\n\n\n\n\n
\n\n\n\n
\n

" . __( 'Wherein is related the droll way in which Don Quixote had himself dubbed a knight.', 'gutenberg' ) . "

\n\n\n\n\n
\n
\n
\n", - 'viewportWidth' => 1000, - 'description' => _x( 'Three small columns of text, each with an outlined button with rounded corners at the bottom.', 'Block pattern description', 'gutenberg' ), + 'title' => __( 'Three columns of text with buttons', 'gutenberg' ), + 'categories' => array( 'columns' ), + 'content' => "\n
\n
\n
\n

" . __( 'Which treats of the character and pursuits of the famous Don Quixote of La Mancha.', 'gutenberg' ) . "

\n\n\n\n\n
\n\n\n\n
\n

" . __( 'Which treats of the first sally the ingenious Don Quixote made from home.', 'gutenberg' ) . "

\n\n\n\n\n
\n\n\n\n
\n

" . __( 'Wherein is related the droll way in which Don Quixote had himself dubbed a knight.', 'gutenberg' ) . "

\n\n\n\n\n
\n
\n
\n", + 'description' => _x( 'Three small columns of text, each with an outlined button with rounded corners at the bottom.', 'Block pattern description', 'gutenberg' ), ); diff --git a/lib/template-loader.php b/lib/template-loader.php index 601b64de7a629b..b886a9f5ab58b6 100644 --- a/lib/template-loader.php +++ b/lib/template-loader.php @@ -151,9 +151,7 @@ function gutenberg_resolve_template( $template_type, $template_hierarchy = array usort( $templates, function ( $template_a, $template_b ) use ( $slug_priorities ) { - $priority_a = $slug_priorities[ $template_a->post_name ] * 2 + ( 'publish' === $template_a->post_status ? 1 : 0 ); - $priority_b = $slug_priorities[ $template_b->post_name ] * 2 + ( 'publish' === $template_b->post_status ? 1 : 0 ); - return $priority_b - $priority_a; + return $slug_priorities[ $template_a->post_name ] - $slug_priorities[ $template_b->post_name ]; } ); diff --git a/package-lock.json b/package-lock.json index 9b0cd4a7d26cee..d68148a2a075fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17330,7 +17330,6 @@ "@wordpress/shortcode": "file:packages/shortcode", "@wordpress/token-list": "file:packages/token-list", "@wordpress/url": "file:packages/url", - "@wordpress/warning": "file:packages/warning", "@wordpress/wordcount": "file:packages/wordcount", "classnames": "^2.2.5", "css-mediaquery": "^0.1.2", @@ -17341,7 +17340,6 @@ "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", "react-spring": "^8.0.19", - "react-transition-group": "^2.9.0", "reakit": "1.1.0", "redux-multi": "^0.1.12", "refx": "^3.0.0", @@ -17696,6 +17694,7 @@ "@wordpress/compose": "file:packages/compose", "@wordpress/core-data": "file:packages/core-data", "@wordpress/data": "file:packages/data", + "@wordpress/data-controls": "file:packages/data-controls", "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", "@wordpress/hooks": "file:packages/hooks", @@ -17715,7 +17714,6 @@ "classnames": "^2.2.5", "lodash": "^4.17.19", "memize": "^1.1.0", - "refx": "^3.0.0", "rememo": "^3.0.0" } }, @@ -32480,6 +32478,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "dev": true, "requires": { "@babel/runtime": "^7.1.2" } @@ -53532,7 +53531,8 @@ "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "dev": true }, "react-merge-refs": { "version": "1.0.0", @@ -54811,6 +54811,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "dev": true, "requires": { "dom-helpers": "^3.4.0", "loose-envify": "^1.4.0", diff --git a/packages/base-styles/_variables.scss b/packages/base-styles/_variables.scss index b24992ba3a084b..ef9731168a52f4 100644 --- a/packages/base-styles/_variables.scss +++ b/packages/base-styles/_variables.scss @@ -57,7 +57,6 @@ $mobile-header-toolbar-height: 44px; $mobile-floating-toolbar-height: 44px; $mobile-floating-toolbar-margin: 8px; $mobile-color-swatch: 48px; -$header-toolbar-min-width: 335px; /** * Shadows. diff --git a/packages/block-directory/src/components/auto-block-uninstaller/index.js b/packages/block-directory/src/components/auto-block-uninstaller/index.js index 12016417bc8851..e35141c5e08581 100644 --- a/packages/block-directory/src/components/auto-block-uninstaller/index.js +++ b/packages/block-directory/src/components/auto-block-uninstaller/index.js @@ -5,8 +5,13 @@ import { unregisterBlockType } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { store as blockDirectoryStore } from '../../store'; + export default function AutoBlockUninstaller() { - const { uninstallBlockType } = useDispatch( 'core/block-directory' ); + const { uninstallBlockType } = useDispatch( blockDirectoryStore ); const shouldRemoveBlockTypes = useSelect( ( select ) => { const { isAutosavingPost, isSavingPost } = select( 'core/editor' ); @@ -14,7 +19,7 @@ export default function AutoBlockUninstaller() { }, [] ); const unusedBlockTypes = useSelect( - ( select ) => select( 'core/block-directory' ).getUnusedBlockTypes(), + ( select ) => select( blockDirectoryStore ).getUnusedBlockTypes(), [] ); diff --git a/packages/block-directory/src/components/downloadable-block-list-item/index.js b/packages/block-directory/src/components/downloadable-block-list-item/index.js index 5d35741af51384..8a88e8593f0adc 100644 --- a/packages/block-directory/src/components/downloadable-block-list-item/index.js +++ b/packages/block-directory/src/components/downloadable-block-list-item/index.js @@ -10,12 +10,13 @@ import DownloadableBlockAuthorInfo from '../downloadable-block-author-info'; import DownloadableBlockHeader from '../downloadable-block-header'; import DownloadableBlockInfo from '../downloadable-block-info'; import DownloadableBlockNotice from '../downloadable-block-notice'; +import { store as blockDirectoryStore } from '../../store'; export default function DownloadableBlockListItem( { item, onClick } ) { const { isLoading, isInstallable } = useSelect( ( select ) => { const { isInstalling, getErrorNoticeForBlock } = select( - 'core/block-directory' + blockDirectoryStore ); const notice = getErrorNoticeForBlock( item.id ); const hasFatal = notice && notice.isFatal; diff --git a/packages/block-directory/src/components/downloadable-block-notice/index.js b/packages/block-directory/src/components/downloadable-block-notice/index.js index 75e7d5a513bca9..f24545954e63ae 100644 --- a/packages/block-directory/src/components/downloadable-block-notice/index.js +++ b/packages/block-directory/src/components/downloadable-block-notice/index.js @@ -5,10 +5,15 @@ import { __ } from '@wordpress/i18n'; import { Button, Notice } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { store as blockDirectoryStore } from '../../store'; + export const DownloadableBlockNotice = ( { block, onClick } ) => { const errorNotice = useSelect( ( select ) => - select( 'core/block-directory' ).getErrorNoticeForBlock( block.id ), + select( blockDirectoryStore ).getErrorNoticeForBlock( block.id ), [ block ] ); diff --git a/packages/block-directory/src/components/downloadable-blocks-list/index.js b/packages/block-directory/src/components/downloadable-blocks-list/index.js index 5aff92c1914a15..d732b7bc800d5f 100644 --- a/packages/block-directory/src/components/downloadable-blocks-list/index.js +++ b/packages/block-directory/src/components/downloadable-blocks-list/index.js @@ -12,9 +12,10 @@ import { useDispatch } from '@wordpress/data'; * Internal dependencies */ import DownloadableBlockListItem from '../downloadable-block-list-item'; +import { store as blockDirectoryStore } from '../../store'; function DownloadableBlocksList( { items, onHover = noop, onSelect } ) { - const { installBlockType } = useDispatch( 'core/block-directory' ); + const { installBlockType } = useDispatch( blockDirectoryStore ); const { setIsInserterOpened } = useDispatch( 'core/edit-post' ); if ( ! items.length ) { diff --git a/packages/block-directory/src/components/downloadable-blocks-panel/index.js b/packages/block-directory/src/components/downloadable-blocks-panel/index.js index 6cdde9efc28c9a..fa6af8b174dbda 100644 --- a/packages/block-directory/src/components/downloadable-blocks-panel/index.js +++ b/packages/block-directory/src/components/downloadable-blocks-panel/index.js @@ -12,6 +12,7 @@ import { speak } from '@wordpress/a11y'; * Internal dependencies */ import DownloadableBlocksList from '../downloadable-blocks-list'; +import { store as blockDirectoryStore } from '../../store'; function DownloadableBlocksPanel( { downloadableItems, @@ -80,7 +81,7 @@ export default compose( [ const { getDownloadableBlocks, isRequestingDownloadableBlocks, - } = select( 'core/block-directory' ); + } = select( blockDirectoryStore ); const hasPermission = select( 'core' ).canUser( 'read', diff --git a/packages/block-directory/src/plugins/get-install-missing/index.js b/packages/block-directory/src/plugins/get-install-missing/index.js index da4337e69249c0..563fd6e845b582 100644 --- a/packages/block-directory/src/plugins/get-install-missing/index.js +++ b/packages/block-directory/src/plugins/get-install-missing/index.js @@ -12,6 +12,7 @@ import { Warning } from '@wordpress/block-editor'; * Internal dependencies */ import InstallButton from './install-button'; +import { store as blockDirectoryStore } from '../../store'; const getInstallMissing = ( OriginalComponent ) => ( props ) => { const { originalName, originalUndelimitedContent } = props.attributes; @@ -19,7 +20,7 @@ const getInstallMissing = ( OriginalComponent ) => ( props ) => { // eslint-disable-next-line react-hooks/rules-of-hooks const { block, hasPermission } = useSelect( ( select ) => { - const { getDownloadableBlocks } = select( 'core/block-directory' ); + const { getDownloadableBlocks } = select( blockDirectoryStore ); const blocks = getDownloadableBlocks( 'block:' + originalName ).filter( ( { name } ) => originalName === name ); diff --git a/packages/block-directory/src/plugins/get-install-missing/install-button.js b/packages/block-directory/src/plugins/get-install-missing/install-button.js index f4985de41846e2..7805589da40490 100644 --- a/packages/block-directory/src/plugins/get-install-missing/install-button.js +++ b/packages/block-directory/src/plugins/get-install-missing/install-button.js @@ -6,11 +6,16 @@ import { Button } from '@wordpress/components'; import { createBlock, getBlockType, parse } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { store as blockDirectoryStore } from '../../store'; + export default function InstallButton( { attributes, block, clientId } ) { const isInstallingBlock = useSelect( ( select ) => - select( 'core/block-directory' ).isInstalling( block.id ) + select( blockDirectoryStore ).isInstalling( block.id ) ); - const { installBlockType } = useDispatch( 'core/block-directory' ); + const { installBlockType } = useDispatch( blockDirectoryStore ); const { replaceBlock } = useDispatch( 'core/block-editor' ); return ( diff --git a/packages/block-directory/src/plugins/installed-blocks-pre-publish-panel/index.js b/packages/block-directory/src/plugins/installed-blocks-pre-publish-panel/index.js index f00528793dc634..a51b68fcaffef2 100644 --- a/packages/block-directory/src/plugins/installed-blocks-pre-publish-panel/index.js +++ b/packages/block-directory/src/plugins/installed-blocks-pre-publish-panel/index.js @@ -10,10 +10,11 @@ import { blockDefault } from '@wordpress/icons'; * Internal dependencies */ import CompactList from '../../components/compact-list'; +import { store as blockDirectoryStore } from '../../store'; export default function InstalledBlocksPrePublishPanel() { const newBlockTypes = useSelect( - ( select ) => select( 'core/block-directory' ).getNewBlockTypes(), + ( select ) => select( blockDirectoryStore ).getNewBlockTypes(), [] ); diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index b8264cfd2e8ee4..e5e7a8a50cee64 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -50,7 +50,6 @@ "@wordpress/shortcode": "file:../shortcode", "@wordpress/token-list": "file:../token-list", "@wordpress/url": "file:../url", - "@wordpress/warning": "file:../warning", "@wordpress/wordcount": "file:../wordcount", "classnames": "^2.2.5", "css-mediaquery": "^0.1.2", @@ -61,7 +60,6 @@ "memize": "^1.1.0", "react-autosize-textarea": "^7.1.0", "react-spring": "^8.0.19", - "react-transition-group": "^2.9.0", "reakit": "1.1.0", "redux-multi": "^0.1.12", "refx": "^3.0.0", diff --git a/packages/block-editor/src/components/block-controls/index.js b/packages/block-editor/src/components/block-controls/index.js index f2b35e4b5301f8..297d3839df4aa9 100644 --- a/packages/block-editor/src/components/block-controls/index.js +++ b/packages/block-editor/src/components/block-controls/index.js @@ -20,24 +20,18 @@ import useDisplayBlockControls from '../use-display-block-controls'; const { Fill, Slot } = createSlotFill( 'BlockControls' ); -function BlockControlsSlot( { __experimentalIsExpanded = false, ...props } ) { +function BlockControlsSlot( props ) { const accessibleToolbarState = useContext( ToolbarContext ); - return ( - - ); + return ; } -function BlockControlsFill( { controls, __experimentalIsExpanded, children } ) { +function BlockControlsFill( { controls, children } ) { if ( ! useDisplayBlockControls() ) { return null; } return ( - + { ( fillProps ) => { // Children passed to BlockControlsFill will not have access to any // React Context whose Provider is part of the BlockControlsSlot tree. @@ -54,9 +48,6 @@ function BlockControlsFill( { controls, __experimentalIsExpanded, children } ) { ); } -const buildSlotName = ( isExpanded ) => - `BlockControls${ isExpanded ? '-expanded' : '' }`; - const BlockControls = BlockControlsFill; BlockControls.Slot = BlockControlsSlot; diff --git a/packages/block-editor/src/components/block-patterns-list/README.md b/packages/block-editor/src/components/block-patterns-list/README.md new file mode 100644 index 00000000000000..b59b0819c8f836 --- /dev/null +++ b/packages/block-editor/src/components/block-patterns-list/README.md @@ -0,0 +1,58 @@ +# Block Patterns List + +The `BlockPatternList` component makes a list of the different registered block patterns. It uses the `BlockPreview` component to display a preview for each block pattern. + +For more infos about blocks patterns, read [this](https://make.wordpress.org/core/2020/07/16/block-patterns-in-wordpress-5-5/). + +![Block patterns sidebar in WordPress 5.5](https://make.wordpress.org/core/files/2020/09/blocks-patterns-sidebar-in-wordpress-5-5.png) + +## Table of contents + +1. [Development guidelines](#development-guidelines) +2. [Related components](#related-components) + +## Development guidelines + +### Usage + +Renders a block patterns list. + +```jsx +import { BlockPatternList } from '@wordpress/block-editor'; + +const MyBlockPatternList = () => ( + +); +``` + +### Props + +#### blockPatterns + +An array of block patterns that can be shown in the block patterns list. + +- Type: `Array` +- Required: Yes + +#### shownPatterns + +An array of shown block patterns objects. + + +- Type: `Array` +- Required: Yes + +#### onClickPattern + +The performed event after a click on a block pattern. In most cases, the pattern is inserted in the block editor. + +- Type: `Function` +- Required: Yes + +## Related components + +Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/block-preview/index.js b/packages/block-editor/src/components/block-preview/index.js index 5727d969f34f12..6978d003b8ebd1 100644 --- a/packages/block-editor/src/components/block-preview/index.js +++ b/packages/block-editor/src/components/block-preview/index.js @@ -19,7 +19,7 @@ import AutoHeightBlockPreview from './auto'; export function BlockPreview( { blocks, __experimentalPadding = 0, - viewportWidth = 700, + viewportWidth = 1200, __experimentalLive = false, __experimentalOnClick, } ) { diff --git a/packages/block-editor/src/components/block-toolbar/README.md b/packages/block-editor/src/components/block-toolbar/README.md new file mode 100644 index 00000000000000..8c0ce66931563e --- /dev/null +++ b/packages/block-editor/src/components/block-toolbar/README.md @@ -0,0 +1,29 @@ +# Block Toolbar + +The `BlockToolbar` component is used to render a toolbar that serves as a wrapper for number of options for each block. + +![Paragraph block toolbar](https://make.wordpress.org/core/files/2020/09/paragraph-block-toolbar.png) + +![Image block toolbar](https://make.wordpress.org/core/files/2020/09/image-block-toolbar.png) + + +## Table of contents + +1. [Development guidelines](#development-guidelines) +2. [Related components](#related-components) + +## Development guidelines + +### Usage + +Displays a block toolbar for a selected block. + +```jsx +import { BlockToolbar } from '@wordpress/block-editor'; + +const MyBlockToolbar = () => +``` + +## Related components + +Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/block-toolbar/expanded-block-controls-container.js b/packages/block-editor/src/components/block-toolbar/expanded-block-controls-container.js deleted file mode 100644 index e1a475c286e88f..00000000000000 --- a/packages/block-editor/src/components/block-toolbar/expanded-block-controls-container.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * External dependencies - */ -import { TransitionGroup, CSSTransition } from 'react-transition-group'; -import { throttle } from 'lodash'; - -/** - * WordPress dependencies - */ -import { useRef, useState, useEffect, useCallback } from '@wordpress/element'; -import warning from '@wordpress/warning'; - -/** - * Internal dependencies - */ -import BlockControls from '../block-controls'; - -function getComputedStyle( node ) { - return node.ownerDocument.defaultView.getComputedStyle( node ); -} - -export default function ExpandedBlockControlsContainer( { - children, - className, -} ) { - return ( - - { ( fills ) => { - return ( - - { children } - - ); - } } - - ); -} - -function ExpandedBlockControlsHandler( { fills, className = '', children } ) { - const containerRef = useRef(); - const fillsRef = useRef(); - const toolbarRef = useRef(); - const [ dimensions, setDimensions ] = useState( {} ); - - const fillsPropRef = useRef(); - fillsPropRef.current = fills; - const resizeToolbar = useCallback( - throttle( () => { - const toolbarContentElement = fillsPropRef.current.length - ? fillsRef.current - : toolbarRef.current; - if ( ! toolbarContentElement ) { - return; - } - toolbarContentElement.style.position = 'absolute'; - toolbarContentElement.style.width = 'auto'; - const contentCSS = getComputedStyle( toolbarContentElement ); - setDimensions( { - width: contentCSS.getPropertyValue( 'width' ), - height: contentCSS.getPropertyValue( 'height' ), - } ); - toolbarContentElement.style.position = ''; - toolbarContentElement.style.width = ''; - }, 100 ), - [] - ); - - useEffect( () => { - const observer = new window.MutationObserver( function ( - mutationsList - ) { - const hasChildList = mutationsList.find( - ( { type } ) => type === 'childList' - ); - if ( hasChildList ) { - resizeToolbar(); - } - } ); - - observer.observe( containerRef.current, { - childList: true, - subtree: true, - } ); - - return () => observer.disconnect(); - }, [] ); - - useEffect( () => { - if ( fills.length > 1 ) { - warning( - `${ fills.length } slots were registered but only one may be displayed.` - ); - } - }, [ fills.length ] ); - - const displayFill = fills[ 0 ]; - return ( -
- - { displayFill ? ( - -
- { displayFill } -
-
- ) : ( - -
- { children } -
-
- ) } -
-
- ); -} diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index bed8b921a2fe91..87cfcad1cf9f1a 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -22,12 +22,8 @@ import BlockControls from '../block-controls'; import BlockFormatControls from '../block-format-controls'; import BlockSettingsMenu from '../block-settings-menu'; import { useShowMoversGestures } from './utils'; -import ExpandedBlockControlsContainer from './expanded-block-controls-container'; -export default function BlockToolbar( { - hideDragHandle, - __experimentalExpandedControl = false, -} ) { +export default function BlockToolbar( { hideDragHandle } ) { const { blockClientIds, blockClientId, @@ -106,12 +102,8 @@ export default function BlockToolbar( { shouldShowMovers && 'is-showing-movers' ); - const Wrapper = __experimentalExpandedControl - ? ExpandedBlockControlsContainer - : 'div'; - return ( - +
{ ! isMultiToolbar && (
@@ -141,6 +133,6 @@ export default function BlockToolbar( { ) } - +
); } diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index bffe229b714e04..c9499b4731dfd0 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -106,31 +106,3 @@ transform: translateY(-($block-toolbar-height + $grid-unit-15)); } } - -.block-editor-block-toolbar-animated-width-container { - position: relative; - overflow: hidden; - transition: width 300ms; -} - -.block-editor-block-toolbar-content-enter { - position: absolute; - top: 0; - left: 0; - width: auto; - opacity: 0; -} -.block-editor-block-toolbar-content-enter-active { - position: absolute; - opacity: 1; - transition: opacity 300ms; -} -.block-editor-block-toolbar-content-exit { - width: auto; - opacity: 1; - pointer-events: none; -} -.block-editor-block-toolbar-content-exit-active { - opacity: 0; - transition: opacity 300ms; -} diff --git a/packages/block-editor/src/components/block-variation-picker/style.scss b/packages/block-editor/src/components/block-variation-picker/style.scss index 1532c50d73b256..882e2748f6c92c 100644 --- a/packages/block-editor/src/components/block-variation-picker/style.scss +++ b/packages/block-editor/src/components/block-variation-picker/style.scss @@ -31,7 +31,7 @@ list-style: none; margin: $grid-unit-10 ( $grid-unit-10 + $grid-unit-15 ) 0 0; flex-shrink: 1; - max-width: 100px; + width: 75px; text-align: center; button { @@ -48,6 +48,7 @@ font-family: $default-font; font-size: 12px; display: block; + line-height: 1.4; } } diff --git a/packages/block-editor/src/components/editor-styles/index.js b/packages/block-editor/src/components/editor-styles/index.js index e5acac522d6515..031df58d5a544a 100644 --- a/packages/block-editor/src/components/editor-styles/index.js +++ b/packages/block-editor/src/components/editor-styles/index.js @@ -13,26 +13,23 @@ import { useEffect } from '@wordpress/element'; */ import transformStyles from '../../utils/transform-styles'; -function EditorStyles( { styles } ) { +export default function useEditorStyles( ref, styles ) { useEffect( () => { const updatedStyles = transformStyles( styles, '.editor-styles-wrapper' ); + const { ownerDocument } = ref.current; const nodes = map( compact( updatedStyles ), ( updatedCSS ) => { - const node = document.createElement( 'style' ); + const node = ownerDocument.createElement( 'style' ); node.innerHTML = updatedCSS; - document.body.appendChild( node ); + ownerDocument.body.appendChild( node ); return node; } ); return () => - nodes.forEach( ( node ) => document.body.removeChild( node ) ); - }, [ styles ] ); - - return null; + nodes.forEach( ( node ) => ownerDocument.body.removeChild( node ) ); + }, [ ref, styles ] ); } - -export default EditorStyles; diff --git a/packages/block-editor/src/components/font-appearance-control/index.js b/packages/block-editor/src/components/font-appearance-control/index.js index 29a6036d4a385a..c6670368a03926 100644 --- a/packages/block-editor/src/components/font-appearance-control/index.js +++ b/packages/block-editor/src/components/font-appearance-control/index.js @@ -3,72 +3,121 @@ */ import { CustomSelectControl } from '@wordpress/components'; import { useMemo } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Control to display unified font style and weight options. * - * @param {Object} props Component props. - * @param {Object} props.value Currently selected combination of font style and weight. - * @param {Object} props.options Object containing weight and style options. - * @param {Function} props.onChange Handles selection change. - * @return {WPElement} Font appearance control. + * @param {Object} props Component props. + * @return {WPElement} Font appearance control. */ -export default function FontAppearanceControl( { value, options, onChange } ) { - const { fontStyle, fontWeight } = value; - const { fontStyles = [], fontWeights = [] } = options; - const hasStylesOrWeights = fontStyles.length > 0 || fontWeights.length > 0; +export default function FontAppearanceControl( props ) { + const { + onChange, + options: { fontStyles = [], fontWeights = [] }, + value: { fontStyle, fontWeight }, + } = props; + const hasStyles = !! fontStyles.length; + const hasWeights = !! fontWeights.length; + const hasStylesOrWeights = hasStyles || hasWeights; + const defaultOption = { + key: 'default', + name: __( 'Default' ), + style: { fontStyle: undefined, fontWeight: undefined }, + }; - // Map font styles and weights to select options. - const selectOptions = useMemo( () => { - const defaultCombo = { fontStyle: undefined, fontWeight: undefined }; - const combinedOptions = [ - { - key: 'default', - name: __( 'Default' ), - style: defaultCombo, - presetStyle: defaultCombo, - }, - ]; + // Combines both font style and weight options into a single dropdown. + const combineOptions = () => { + const combinedOptions = [ defaultOption ]; fontStyles.forEach( ( { name: styleName, slug: styleSlug } ) => { fontWeights.forEach( ( { name: weightName, slug: weightSlug } ) => { + const optionName = + styleSlug === 'normal' + ? weightName + : sprintf( + /* translators: 1: Font weight name. 2: Font style name. */ + __( '%1$s %2$s' ), + weightName, + styleName + ); + combinedOptions.push( { key: `${ weightSlug }-${ styleSlug }`, - name: - styleSlug === 'normal' - ? weightName - : `${ weightName } ${ styleName }`, - // style applies font appearance to the individual select option. + name: optionName, style: { fontStyle: styleSlug, fontWeight: weightSlug }, - // presetStyle are the actual typography styles that should be given to onChange. - presetStyle: { - fontStyle: `var:preset|font-style|${ styleSlug }`, - fontWeight: `var:preset|font-weight|${ weightSlug }`, - }, } ); } ); } ); return combinedOptions; - }, [ options ] ); + }; + + // Generates select options for font styles only. + const styleOptions = () => { + const combinedOptions = [ defaultOption ]; + fontStyles.forEach( ( { name, slug } ) => { + combinedOptions.push( { + key: slug, + name, + style: { fontStyle: slug, fontWeight: undefined }, + } ); + } ); + return combinedOptions; + }; + + // Generates select options for font weights only. + const weightOptions = () => { + const combinedOptions = [ defaultOption ]; + fontWeights.forEach( ( { name, slug } ) => { + combinedOptions.push( { + key: slug, + name, + style: { fontStyle: undefined, fontWeight: slug }, + } ); + } ); + return combinedOptions; + }; + + // Map font styles and weights to select options. + const selectOptions = useMemo( () => { + if ( hasStyles && hasWeights ) { + return combineOptions(); + } + return hasStyles ? styleOptions() : weightOptions(); + }, [ props.options ] ); + + // Find current selection by comparing font style & weight against options. const currentSelection = selectOptions.find( ( option ) => - option.presetStyle.fontStyle === fontStyle && - option.presetStyle.fontWeight === fontWeight + option.style.fontStyle === fontStyle && + option.style.fontWeight === fontWeight ); + // Adjusts field label in case either styles or weights are disabled. + const getLabel = () => { + if ( ! hasStyles ) { + return __( 'Font weight' ); + } + + if ( ! hasWeights ) { + return __( 'Font style' ); + } + + return __( 'Appearance' ); + }; + return (
{ hasStylesOrWeights && ( - onChange( selectedItem.presetStyle ) + onChange( selectedItem.style ) } /> ) } diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 150a67eb14a492..9d619e94ba6e3f 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -97,7 +97,7 @@ export { useClipboardHandler as __unstableUseClipboardHandler, } from './copy-handler'; export { default as DefaultBlockAppender } from './default-block-appender'; -export { default as __unstableEditorStyles } from './editor-styles'; +export { default as __unstableUseEditorStyles } from './editor-styles'; export { default as Inserter } from './inserter'; export { default as __experimentalLibrary } from './inserter/library'; export { default as __experimentalSearchForm } from './inserter/search-form'; @@ -119,6 +119,7 @@ export { } from './typewriter'; export { default as Warning } from './warning'; export { default as WritingFlow } from './writing-flow'; +export { useCanvasClickRedirect as __unstableUseCanvasClickRedirect } from './use-canvas-click-redirect'; /* * State Related Components diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 2245d16a1511ba..29a3b085c09ba1 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -26,6 +26,7 @@ export { default as MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, + MEDIA_TYPE_ANY, } from './media-upload'; export { default as MediaUploadProgress } from './media-upload-progress'; export { default as BlockMediaUpdateProgress } from './block-media-update-progress'; diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 6870de16c65101..df7ec3e8ec8a9a 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -157,6 +157,7 @@ function InserterMenu( { @@ -150,7 +147,7 @@ function InserterSearchResults( { <__experimentalInserterMenuExtension.Slot fillProps={ { onSelect: onSelectBlockType, - onHover: onToggleInsertionPoint, + onHover, filterValue, hasItems, } } diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js index d413171cd114be..d3fabd74238539 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -23,6 +23,7 @@ import { export const MEDIA_TYPE_IMAGE = 'image'; export const MEDIA_TYPE_VIDEO = 'video'; +export const MEDIA_TYPE_ANY = 'any'; export const OPTION_TAKE_VIDEO = __( 'Take a Video' ); export const OPTION_TAKE_PHOTO = __( 'Take a Photo' ); @@ -86,7 +87,7 @@ export class MediaUpload extends React.Component { id: mediaSources.siteMediaLibrary, value: mediaSources.siteMediaLibrary, label: __( 'WordPress Media Library' ), - types: [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO ], + types: [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_ANY ], icon: wordpress, mediaLibrary: true, }; @@ -129,8 +130,9 @@ export class MediaUpload extends React.Component { const isOneType = allowedTypes.length === 1; const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); + const isAnyType = isOneType && allowedTypes.includes( MEDIA_TYPE_ANY ); - if ( isImage || ! isOneType ) { + if ( isImage || ! isOneType || isAnyType ) { return image; } else if ( isVideo ) { return video; @@ -164,6 +166,8 @@ export class MediaUpload extends React.Component { const isOneType = allowedTypes.length === 1; const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); + const isAnyType = isOneType && allowedTypes.includes( MEDIA_TYPE_ANY ); + const isImageOrVideo = allowedTypes.length === 2 && allowedTypes.includes( MEDIA_TYPE_IMAGE ) && @@ -190,6 +194,8 @@ export class MediaUpload extends React.Component { } else { pickerTitle = __( 'Choose image or video' ); } + } else if ( isAnyType ) { + pickerTitle = __( 'Choose file' ); } const getMediaOptions = () => ( diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 13cb0a801da1ae..b3919feb052ef7 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -120,6 +120,7 @@ function RichTextWrapper( __unstableOnSplitMiddle: onSplitMiddle, identifier, preserveWhiteSpace, + __unstablePastePlainText: pastePlainText, __unstableEmbedURLOnPaste, __unstableDisableFormats: disableFormats, disableLineBreaks, @@ -408,6 +409,11 @@ function RichTextWrapper( const onPaste = useCallback( ( { value, onChange, html, plainText, files, activeFormats } ) => { + if ( pastePlainText ) { + onChange( insert( value, create( { text: plainText } ) ) ); + return; + } + // Only process file if no HTML is present. // Note: a pasted file may have the URL as plain text. if ( files && files.length && ! html ) { @@ -503,6 +509,7 @@ function RichTextWrapper( __unstableEmbedURLOnPaste, multiline, preserveWhiteSpace, + pastePlainText, ] ); diff --git a/packages/block-editor/src/components/use-canvas-click-redirect/index.js b/packages/block-editor/src/components/use-canvas-click-redirect/index.js new file mode 100644 index 00000000000000..6b8280e95ab093 --- /dev/null +++ b/packages/block-editor/src/components/use-canvas-click-redirect/index.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import { overEvery, findLast } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useEffect } from '@wordpress/element'; +import { focus, isTextField, placeCaretAtHorizontalEdge } from '@wordpress/dom'; + +/** + * Given an element, returns true if the element is a tabbable text field, or + * false otherwise. + * + * @param {Element} element Element to test. + * + * @return {boolean} Whether element is a tabbable text field. + */ +const isTabbableTextField = overEvery( [ + isTextField, + focus.tabbable.isTabbableIndex, +] ); + +export function useCanvasClickRedirect( ref ) { + useEffect( () => { + function onMouseDown( event ) { + // Only handle clicks on the canvas, not the content. + if ( event.target !== ref.current ) { + return; + } + + const focusableNodes = focus.focusable.find( ref.current ); + const target = findLast( focusableNodes, isTabbableTextField ); + + if ( ! target ) { + return; + } + + placeCaretAtHorizontalEdge( target, true ); + event.preventDefault(); + } + + ref.current.addEventListener( 'mousedown', onMouseDown ); + + return () => { + ref.current.addEventListener( 'mousedown', onMouseDown ); + }; + }, [] ); +} diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index d48428a80cf9c9..030fef109d4868 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { overEvery, find, findLast, reverse, first, last } from 'lodash'; +import { find, reverse, first, last } from 'lodash'; import classnames from 'classnames'; /** @@ -49,19 +49,6 @@ function getComputedStyle( node ) { return node.ownerDocument.defaultView.getComputedStyle( node ); } -/** - * Given an element, returns true if the element is a tabbable text field, or - * false otherwise. - * - * @param {Element} element Element to test. - * - * @return {boolean} Whether element is a tabbable text field. - */ -const isTabbableTextField = overEvery( [ - isTextField, - focus.tabbable.isTabbableIndex, -] ); - /** * Returns true if the element should consider edge navigation upon a keyboard * event of the given directional key code, or false otherwise. @@ -684,14 +671,6 @@ export default function WritingFlow( { children } ) { } } - function focusLastTextField() { - const focusableNodes = focus.focusable.find( container.current ); - const target = findLast( focusableNodes, isTabbableTextField ); - if ( target ) { - placeCaretAtHorizontalEdge( target, true ); - } - } - useEffect( () => { if ( hasMultiSelection && ! isMultiSelecting ) { multiSelectionContainer.current.focus(); @@ -746,12 +725,6 @@ export default function WritingFlow( { children } ) { multiSelectionContainer={ multiSelectionContainer } isReverse /> -
); /* eslint-enable jsx-a11y/no-static-element-interactions */ diff --git a/packages/block-editor/src/components/writing-flow/style.scss b/packages/block-editor/src/components/writing-flow/style.scss deleted file mode 100644 index 422b378f2e8e2e..00000000000000 --- a/packages/block-editor/src/components/writing-flow/style.scss +++ /dev/null @@ -1,8 +0,0 @@ -.block-editor-writing-flow { - display: flex; - flex-direction: column; -} - -.block-editor-writing-flow__click-redirect { - cursor: text; -} diff --git a/packages/block-editor/src/hooks/font-appearance.js b/packages/block-editor/src/hooks/font-appearance.js index bbad86a20f133b..e2055649316375 100644 --- a/packages/block-editor/src/hooks/font-appearance.js +++ b/packages/block-editor/src/hooks/font-appearance.js @@ -11,10 +11,14 @@ import useEditorFeature from '../components/use-editor-feature'; import { cleanEmptyObject } from './utils'; /** - * Key within block settings' support array indicating support for font - * appearance options e.g. font weight and style. + * Key within block settings' support array indicating support for font style. */ -export const FONT_APPEARANCE_SUPPORT_KEY = '__experimentalFontAppearance'; +export const FONT_STYLE_SUPPORT_KEY = '__experimentalFontStyle'; + +/** + * Key within block settings' support array indicating support for font weight. + */ +export const FONT_WEIGHT_SUPPORT_KEY = '__experimentalFontWeight'; /** * Inspector control panel containing the font appearance options. @@ -30,53 +34,123 @@ export function FontAppearanceEdit( props ) { const fontStyles = useEditorFeature( 'typography.fontStyles' ); const fontWeights = useEditorFeature( 'typography.fontWeights' ); - const isDisabled = useIsFontAppearanceDisabled( props ); + const isFontStyleDisabled = useIsFontStyleDisabled( props ); + const isFontWeightDisabled = useIsFontWeightDisabled( props ); - if ( isDisabled ) { + if ( isFontStyleDisabled && isFontWeightDisabled ) { return null; } const onChange = ( newStyles ) => { + // Match style selection with preset and create CSS var style if appropriate. + const presetStyle = fontStyles.find( + ( { slug } ) => slug === newStyles.fontStyle + ); + const newFontStyle = presetStyle + ? `var:preset|font-style|${ presetStyle.slug }` + : undefined; + + // Match weight selection with preset and create CSS var style if appropriate. + const presetWeight = fontWeights.find( + ( { slug } ) => slug === newStyles.fontWeight + ); + const newFontWeight = presetWeight + ? `var:preset|font-weight|${ presetWeight.slug }` + : undefined; + setAttributes( { style: cleanEmptyObject( { ...style, typography: { ...style?.typography, - ...newStyles, + fontStyle: newFontStyle, + fontWeight: newFontWeight, }, } ), } ); }; - const currentSelection = { - fontStyle: style?.typography?.fontStyle, - fontWeight: style?.typography?.fontWeight, - }; + const fontStyle = getFontAppearanceValueFromStyle( + fontStyles, + style?.typography?.fontStyle + ); + + const fontWeight = getFontAppearanceValueFromStyle( + fontWeights, + style?.typography?.fontWeight + ); return ( ); } /** - * Checks if font appearance support has been disabled. + * Checks if font style support has been disabled either by not opting in for + * support or by failing to provide preset styles. * * @param {Object} props Block properties. * @param {string} props.name Name for the block type. - * @return {boolean} Whether font appearance support has been disabled. + * @return {boolean} Whether font style support has been disabled. */ -export function useIsFontAppearanceDisabled( { name: blockName } = {} ) { - const notSupported = ! hasBlockSupport( - blockName, - FONT_APPEARANCE_SUPPORT_KEY - ); +export function useIsFontStyleDisabled( { name: blockName } = {} ) { + const styleSupport = hasBlockSupport( blockName, FONT_STYLE_SUPPORT_KEY ); const fontStyles = useEditorFeature( 'typography.fontStyles' ); + + return ! styleSupport || ! fontStyles?.length; +} + +/** + * Checks if font weight support has been disabled either by not opting in for + * support or by failing to provide preset weights. + * + * @param {Object} props Block properties. + * @param {string} props.name Name for the block type. + * @return {boolean} Whether font weight support has been disabled. + */ +export function useIsFontWeightDisabled( { name: blockName } = {} ) { + const weightSupport = hasBlockSupport( blockName, FONT_WEIGHT_SUPPORT_KEY ); const fontWeights = useEditorFeature( 'typography.fontWeights' ); - const hasFontAppearance = !! fontStyles?.length && !! fontWeights?.length; - return notSupported || ! hasFontAppearance; + return ! weightSupport || ! fontWeights?.length; +} + +/** + * Checks if font appearance support has been disabled. + * + * @param {Object} props Block properties. + * @return {boolean} Whether font appearance support has been disabled. + */ +export function useIsFontAppearanceDisabled( props ) { + const stylesDisabled = useIsFontStyleDisabled( props ); + const weightsDisabled = useIsFontWeightDisabled( props ); + + return stylesDisabled && weightsDisabled; } + +/** + * Extracts the current selection, if available, from the CSS variable set + * within a style attribute property e.g. `style.typography.fontStyle` + * or `style.typography.fontWeight`. + * + * @param {Array} presets Available preset options. + * @param {string} style Style attribute value to parse + * @return {string} Actual CSS property value. + */ +const getFontAppearanceValueFromStyle = ( presets, style ) => { + if ( ! style ) { + return undefined; + } + + const parsedValue = style.slice( style.lastIndexOf( '|' ) + 1 ); + const preset = presets.find( ( { slug } ) => slug === parsedValue ); + + return preset?.slug || style; +}; diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index db17bd98aaca9f..dfc27748acb00f 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -18,7 +18,8 @@ import { useIsLineHeightDisabled, } from './line-height'; import { - FONT_APPEARANCE_SUPPORT_KEY, + FONT_STYLE_SUPPORT_KEY, + FONT_WEIGHT_SUPPORT_KEY, FontAppearanceEdit, useIsFontAppearanceDisabled, } from './font-appearance'; @@ -43,8 +44,9 @@ import { export const TYPOGRAPHY_SUPPORT_KEYS = [ LINE_HEIGHT_SUPPORT_KEY, - FONT_APPEARANCE_SUPPORT_KEY, FONT_SIZE_SUPPORT_KEY, + FONT_STYLE_SUPPORT_KEY, + FONT_WEIGHT_SUPPORT_KEY, FONT_FAMILY_SUPPORT_KEY, TEXT_DECORATION_SUPPORT_KEY, TEXT_TRANSFORM_SUPPORT_KEY, diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 5421186cbdff53..1ea21ed4574523 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -52,7 +52,6 @@ @import "./components/url-input/style.scss"; @import "./components/url-popover/style.scss"; @import "./components/warning/style.scss"; -@import "./components/writing-flow/style.scss"; @import "./hooks/anchor.scss"; // This tag marks the end of the styles that apply to editing canvas contents and need to be manipulated when we resize the editor. diff --git a/packages/block-editor/src/utils/get-paste-event-data.js b/packages/block-editor/src/utils/get-paste-event-data.js index b840de2964250c..0db158c5f6e8e6 100644 --- a/packages/block-editor/src/utils/get-paste-event-data.js +++ b/packages/block-editor/src/utils/get-paste-event-data.js @@ -25,9 +25,9 @@ export function getPasteEventData( { clipboardData } ) { } } - const files = [ - ...getFilesFromDataTransfer( clipboardData ), - ].filter( ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ); + const files = getFilesFromDataTransfer( + clipboardData + ).filter( ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ); // Only process files if no HTML is present. // A pasted file may have the URL as plain text. diff --git a/packages/block-editor/src/utils/transform-styles/index.js b/packages/block-editor/src/utils/transform-styles/index.js index b0f1d66fe55111..763554707bd2e7 100644 --- a/packages/block-editor/src/utils/transform-styles/index.js +++ b/packages/block-editor/src/utils/transform-styles/index.js @@ -23,20 +23,23 @@ import wrap from './transforms/wrap'; * @return {Array} converted rules. */ const transformStyles = ( styles, wrapperClassName = '' ) => { - return map( styles, ( { css, baseURL } ) => { - const transforms = []; - if ( wrapperClassName ) { - transforms.push( wrap( wrapperClassName ) ); - } - if ( baseURL ) { - transforms.push( urlRewrite( baseURL ) ); - } - if ( transforms.length ) { - return traverse( css, compose( transforms ) ); - } + return map( + styles, + ( { css, baseURL, __experimentalNoWrapper = false } ) => { + const transforms = []; + if ( wrapperClassName && ! __experimentalNoWrapper ) { + transforms.push( wrap( wrapperClassName ) ); + } + if ( baseURL ) { + transforms.push( urlRewrite( baseURL ) ); + } + if ( transforms.length ) { + return traverse( css, compose( transforms ) ); + } - return css; - } ); + return css; + } + ); }; export default transformStyles; diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index af64ac7b80645f..51105cd4262a99 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -19,6 +19,7 @@ import { BlockControls, useBlockProps, } from '@wordpress/block-editor'; +import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; /** * Internal dependencies @@ -51,7 +52,7 @@ export default function ReusableBlockEdit( { isSaving: select( 'core' ).isSavingEntityRecord( ...recordArgs ), canUserUpdate: select( 'core' ).canUser( 'update', 'blocks', ref ), isEditing: select( - 'core/reusable-blocks' + reusableBlocksStore ).__experimentalIsEditingReusableBlock( clientId ), settings: select( 'core/block-editor' ).getSettings(), } ), @@ -60,7 +61,7 @@ export default function ReusableBlockEdit( { const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' ); const { __experimentalSetEditingReusableBlock } = useDispatch( - 'core/reusable-blocks' + reusableBlocksStore ); const setIsEditing = useCallback( ( value ) => { @@ -71,7 +72,7 @@ export default function ReusableBlockEdit( { const { __experimentalConvertBlockToStatic: convertBlockToStatic, - } = useDispatch( 'core/reusable-blocks' ); + } = useDispatch( reusableBlocksStore ); const { createSuccessNotice, createErrorNotice } = useDispatch( 'core/notices' diff --git a/packages/block-library/src/block/editor.scss b/packages/block-library/src/block/editor.scss index b5c828e0680d5a..b3eb1a67e99459 100644 --- a/packages/block-library/src/block/editor.scss +++ b/packages/block-library/src/block/editor.scss @@ -1,8 +1,4 @@ .edit-post-visual-editor .block-library-block__reusable-block-container { - .block-editor-writing-flow__click-redirect { - min-height: auto; - } - // Unset the padding that root containers get when they're actually root containers. .is-root-container { padding-left: 0; diff --git a/packages/block-library/src/code/block.json b/packages/block-library/src/code/block.json index d9f37ed061f808..e6a0c94e51ba37 100644 --- a/packages/block-library/src/code/block.json +++ b/packages/block-library/src/code/block.json @@ -10,6 +10,7 @@ } }, "supports": { - "anchor": true + "anchor": true, + "fontSize": true } } diff --git a/packages/block-library/src/code/edit.js b/packages/block-library/src/code/edit.js index b8960d242ee886..4153b14d753cdc 100644 --- a/packages/block-library/src/code/edit.js +++ b/packages/block-library/src/code/edit.js @@ -16,6 +16,7 @@ export default function CodeEdit( { attributes, setAttributes, onRemove } ) { placeholder={ __( 'Write code…' ) } aria-label={ __( 'Code' ) } preserveWhiteSpace + __unstablePastePlainText /> ); diff --git a/packages/block-library/src/code/editor.scss b/packages/block-library/src/code/editor.scss deleted file mode 100644 index bc47a4f35ee9da..00000000000000 --- a/packages/block-library/src/code/editor.scss +++ /dev/null @@ -1,4 +0,0 @@ -.wp-block-code > code { - // PlainText cannot be an inline element yet. - display: block; -} diff --git a/packages/block-library/src/code/style.scss b/packages/block-library/src/code/style.scss index 3961cfbe8e7d96..b93a5501d5b231 100644 --- a/packages/block-library/src/code/style.scss +++ b/packages/block-library/src/code/style.scss @@ -1,5 +1,10 @@ // Provide a minimum of overflow handling. -.wp-block-code code { - white-space: pre-wrap; - overflow-wrap: break-word; +.wp-block-code { + font-size: var(--wp--preset--font-size--extra-small, 0.9em); + + code { + display: block; + white-space: pre-wrap; + overflow-wrap: break-word; + } } diff --git a/packages/block-library/src/code/theme.scss b/packages/block-library/src/code/theme.scss index be1469328db3dc..4dfc5a41a988e3 100644 --- a/packages/block-library/src/code/theme.scss +++ b/packages/block-library/src/code/theme.scss @@ -1,6 +1,5 @@ .wp-block-code { font-family: $editor-html-font; - font-size: 0.9em; color: $gray-900; padding: 0.8em 1em; border: 1px solid $gray-300; diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 1f7387b6b89bb1..e1f73e1edf8c73 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -9,7 +9,6 @@ @import "./button/editor.scss"; @import "./buttons/editor.scss"; @import "./categories/editor.scss"; -@import "./code/editor.scss"; @import "./columns/editor.scss"; @import "./cover/editor.scss"; @import "./embed/editor.scss"; diff --git a/packages/block-library/src/file/edit.native.js b/packages/block-library/src/file/edit.native.js index 82d6a683626b04..09fff8ce8ea74c 100644 --- a/packages/block-library/src/file/edit.native.js +++ b/packages/block-library/src/file/edit.native.js @@ -1,12 +1,17 @@ /** * External dependencies */ -import { View, Text, Clipboard } from 'react-native'; +import { View, Clipboard, TouchableWithoutFeedback, Text } from 'react-native'; import React from 'react'; /** * WordPress dependencies */ +import { + requestImageFailedRetryDialog, + requestImageUploadCancelDialog, + mediaUploadSync, +} from '@wordpress/react-native-bridge'; import { BlockIcon, MediaPlaceholder, @@ -16,6 +21,7 @@ import { BlockControls, MediaUpload, InspectorControls, + MEDIA_TYPE_ANY, } from '@wordpress/block-editor'; import { ToolbarButton, @@ -24,6 +30,7 @@ import { ToggleControl, BottomSheet, SelectControl, + Icon, } from '@wordpress/components'; import { file as icon, @@ -31,11 +38,13 @@ import { button, external, link, + warning, } from '@wordpress/icons'; import { Component } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { getProtocol } from '@wordpress/url'; /** * Internal dependencies @@ -43,6 +52,7 @@ import { withSelect } from '@wordpress/data'; import styles from './style.scss'; const URL_COPIED_NOTIFICATION_DURATION_MS = 1500; +const MIN_WIDTH = 40; export class FileEdit extends Component { constructor( props ) { @@ -50,10 +60,14 @@ export class FileEdit extends Component { this.state = { isUploadInProgress: false, + isSidebarLinkSettings: false, + placeholderTextWidth: 0, + maxWidth: 0, }; this.timerRef = null; + this.onLayout = this.onLayout.bind( this ); this.onSelectFile = this.onSelectFile.bind( this ); this.onChangeFileName = this.onChangeFileName.bind( this ); this.onChangeDownloadButtonText = this.onChangeDownloadButtonText.bind( @@ -63,6 +77,9 @@ export class FileEdit extends Component { this.finishMediaUploadWithSuccess = this.finishMediaUploadWithSuccess.bind( this ); + this.finishMediaUploadWithFailure = this.finishMediaUploadWithFailure.bind( + this + ); this.getFileComponent = this.getFileComponent.bind( this ); this.onChangeDownloadButtonVisibility = this.onChangeDownloadButtonVisibility.bind( this @@ -75,6 +92,9 @@ export class FileEdit extends Component { this.onChangeLinkDestinationOption = this.onChangeLinkDestinationOption.bind( this ); + this.onShowLinkSettings = this.onShowLinkSettings.bind( this ); + this.onFilePressed = this.onFilePressed.bind( this ); + this.mediaUploadStateReset = this.mediaUploadStateReset.bind( this ); } componentDidMount() { @@ -86,12 +106,30 @@ export class FileEdit extends Component { downloadButtonText: _x( 'Download', 'button label' ), } ); } + + if ( + attributes.id && + attributes.url && + getProtocol( attributes.url ) === 'file:' + ) { + mediaUploadSync(); + } } componentWillUnmount() { clearTimeout( this.timerRef ); } + componentDidUpdate( prevProps ) { + if ( + prevProps.isSidebarOpened && + ! this.props.isSidebarOpened && + this.state.isSidebarLinkSettings + ) { + this.setState( { isSidebarLinkSettings: false } ); + } + } + onSelectFile( media ) { this.props.setAttributes( { href: media.url, @@ -158,22 +196,29 @@ export class FileEdit extends Component { this.setState( { isUploadInProgress: false } ); } + finishMediaUploadWithFailure( payload ) { + this.props.setAttributes( { id: payload.mediaId } ); + this.setState( { isUploadInProgress: false } ); + } + mediaUploadStateReset() { const { setAttributes } = this.props; - setAttributes( { id: null, url: null } ); + setAttributes( { + id: null, + href: null, + textLinkHref: null, + fileName: null, + } ); this.setState( { isUploadInProgress: false } ); } - getErrorComponent( retryMessage ) { - return ( - retryMessage && ( - - - { retryMessage } - - - ) + onShowLinkSettings() { + this.setState( + { + isSidebarLinkSettings: true, + }, + this.props.openSidebar ); } @@ -186,6 +231,11 @@ export class FileEdit extends Component { icon={ replace } onClick={ open } /> + ); @@ -199,6 +249,7 @@ export class FileEdit extends Component { ) { let linkDestinationOptions = [ { value: href, label: __( 'URL' ) } ]; const attachmentPage = media && media.link; + const { isSidebarLinkSettings } = this.state; if ( attachmentPage ) { linkDestinationOptions = [ @@ -222,7 +273,9 @@ export class FileEdit extends Component { return ( - + { isSidebarLinkSettings || ( + + ) } - + { ! isSidebarLinkSettings && ( + + ) } { + const textWidth = + nativeEvent.lines[ 0 ] && nativeEvent.lines[ 0 ].width; + if ( textWidth && textWidth !== placeholderTextWidth ) { + this.setState( { + placeholderTextWidth: Math.min( + textWidth, + maxWidth + ), + } ); + } + } } + > + { placeholderText } + + ); + } + getFileComponent( openMediaOptions, getMediaOptions ) { - const { attributes, media } = this.props; + const { attributes, media, isSelected } = this.props; + const { isButtonFocused, placeholderTextWidth } = this.state; const { fileName, @@ -296,13 +400,21 @@ export class FileEdit extends Component { align, } = attributes; - const dimmedStyle = - this.state.isUploadInProgress && styles.disabledButton; - const finalButtonStyle = Object.assign( - {}, - styles.defaultButton, - dimmedStyle - ); + const minWidth = + isButtonFocused || + ( ! isButtonFocused && + downloadButtonText && + downloadButtonText !== '' ) + ? MIN_WIDTH + : placeholderTextWidth; + + const placeholderText = + isButtonFocused || + ( ! isButtonFocused && + downloadButtonText && + downloadButtonText !== '' ) + ? '' + : __( 'Add text…' ); return ( {} } + onFinishMediaUploadWithFailure={ + this.finishMediaUploadWithFailure + } onMediaUploadStateReset={ this.mediaUploadStateReset } - renderContent={ ( { - isUploadInProgress, - isUploadFailed, - retryMessage, - } ) => { - if ( isUploadFailed ) { - return this.getErrorComponent( retryMessage ); - } + renderContent={ ( { isUploadInProgress, isUploadFailed } ) => { + const dimmedStyle = + ( this.state.isUploadInProgress || isUploadFailed ) && + styles.disabledButton; + const finalButtonStyle = [ + styles.defaultButton, + dimmedStyle, + ]; + + const errorIconStyle = Object.assign( + {}, + styles.errorIcon, + styles.uploadFailed + ); return ( - - { isUploadInProgress || - this.getToolbarEditButton( openMediaOptions ) } - { getMediaOptions() } - { this.getInspectorControls( - attributes, - media, - isUploadInProgress, - isUploadFailed - ) } - + + { this.getPlaceholderWidth( placeholderText ) } + { isUploadInProgress || + this.getToolbarEditButton( + openMediaOptions + ) } + { getMediaOptions() } + { this.getInspectorControls( + attributes, + media, + isUploadInProgress, + isUploadFailed ) } - /> - { showDownloadButton && ( - - + <RichText + __unstableMobileNoFocusOnMount + onChange={ this.onChangeFileName } + placeholder={ __( 'File name' ) } + rootTagsToEliminate={ [ 'p' ] } + tagName="p" + underlineColorAndroid="transparent" + value={ fileName } + deleteEnter={ true } + textAlign={ this.getTextAlignmentForAlignment( + align + ) } /> + { isUploadFailed && ( + <View style={ styles.errorContainer }> + <Icon + icon={ warning } + style={ errorIconStyle } + /> + <PlainText + value={ __( 'Error' ) } + style={ styles.uploadFailed } + /> + </View> + ) } </View> - ) } - </View> + { showDownloadButton && ( + <View + style={ [ + finalButtonStyle, + this.getStyleForAlignment( align ), + ] } + > + <RichText + withoutInteractiveFormatting + __unstableMobileNoFocusOnMount + rootTagsToEliminate={ [ 'p' ] } + tagName="p" + textAlign="center" + minWidth={ minWidth } + maxWidth={ this.state.maxWidth } + deleteEnter={ true } + style={ styles.buttonText } + value={ downloadButtonText } + placeholder={ placeholderText } + unstableOnFocus={ () => + this.setState( { + isButtonFocused: true, + } ) + } + onBlur={ () => + this.setState( { + isButtonFocused: false, + } ) + } + selectionColor={ + styles.buttonText.color + } + placeholderTextColor={ + styles.placeholderTextColor + .color + } + underlineColorAndroid="transparent" + onChange={ + this.onChangeDownloadButtonText + } + /> + </View> + ) } + </View> + </TouchableWithoutFeedback> ); } } /> @@ -383,14 +554,14 @@ export class FileEdit extends Component { } } onSelect={ this.onSelectFile } onFocus={ this.props.onFocus } - allowedTypes={ [ 'other' ] } + allowedTypes={ [ MEDIA_TYPE_ANY ] } /> ); } return ( <MediaUpload - allowedTypes={ [ 'other' ] } + allowedTypes={ [ MEDIA_TYPE_ANY ] } isReplacingMedia={ true } onSelect={ this.onSelectFile } render={ ( { open, getMediaOptions } ) => { @@ -405,9 +576,17 @@ export default compose( [ withSelect( ( select, props ) => { const { attributes } = props; const { id } = attributes; + const { isEditorSidebarOpened } = select( 'core/edit-post' ); return { media: id === undefined ? undefined : select( 'core' ).getMedia( id ), + isSidebarOpened: isEditorSidebarOpened(), + }; + } ), + withDispatch( ( dispatch ) => { + const { openGeneralSidebar } = dispatch( 'core/edit-post' ); + return { + openSidebar: () => openGeneralSidebar( 'edit-post/block' ), }; } ), withPreferredColorScheme, diff --git a/packages/block-library/src/file/style.native.scss b/packages/block-library/src/file/style.native.scss index f3a83da6c8404c..f60649577c6d78 100644 --- a/packages/block-library/src/file/style.native.scss +++ b/packages/block-library/src/file/style.native.scss @@ -1,7 +1,6 @@ .defaultButton { border-radius: $border-width * 4; padding: $block-spacing * 2; - border-width: $border-width; margin-top: $grid-unit-20; background-color: $button-fallback-bg; } @@ -9,7 +8,10 @@ .buttonText { background-color: transparent; color: $white; - font-size: 16; + padding: 0; + font-size: 16px; + padding-left: $grid-unit-20; + padding-right: $grid-unit-20; } .disabledButton { @@ -34,3 +36,29 @@ .actionButtonDark { color: $blue-30; } + +.errorContainer { + flex-direction: row; + align-items: center; + padding-top: 4px; +} + +.errorIcon { + margin-left: -4px; +} + +.uploadFailed { + padding: 0; + color: $alert-red; +} + +.placeholderTextColor { + color: rgba($color: $white, $alpha: 0.43); +} + +.placeholder { + font-family: $default-regular-font; + min-height: 22px; + font-size: 16px; + display: none; +} diff --git a/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap new file mode 100644 index 00000000000000..010fb9863b618d --- /dev/null +++ b/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap @@ -0,0 +1,388 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`File block renders file error state without crashing 1`] = ` +<View + pointerEvents="box-none" +> + <View + accessible={true} + focusable={true} + onClick={[Function]} + onLayout={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + > + <Text + onTextLayout={[Function]} + style={ + Object { + "color": "gray", + } + } + > + + </Text> + Modal + <View> + <View> + <RCTAztecView + accessible={true} + activeFormats={Array []} + blockType={ + Object { + "tag": "p", + } + } + deleteEnter={true} + disableEditingMenu={false} + focusable={true} + fontFamily="serif" + isMultiline={false} + maxImagesWidth={200} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onSelectionChange={[Function]} + onStartShouldSetResponder={[Function]} + placeholder="File name" + placeholderTextColor="gray" + style={ + Object { + "backgroundColor": undefined, + "maxWidth": undefined, + "minHeight": 0, + } + } + text={ + Object { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "text": "<p >File name</p>", + } + } + textAlign="left" + triggerKeyCodes={Array []} + /> + </View> + <View> + Svg + <TextInput + allowFontScaling={true} + fontFamily="serif" + onChange={[Function]} + rejectResponderTermination={true} + scrollEnabled={false} + style={ + Object { + "fontFamily": "serif", + } + } + underlineColorAndroid="transparent" + value="Error" + /> + </View> + </View> + <View + style={ + Array [ + Array [ + undefined, + undefined, + ], + Object { + "alignSelf": "flex-start", + }, + ] + } + > + <View> + <RCTAztecView + accessible={true} + activeFormats={Array []} + blockType={ + Object { + "tag": "p", + } + } + color="white" + deleteEnter={true} + disableEditingMenu={false} + focusable={true} + fontFamily="serif" + isMultiline={false} + maxImagesWidth={200} + minWidth={40} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onSelectionChange={[Function]} + onStartShouldSetResponder={[Function]} + placeholder="" + placeholderTextColor="white" + selectionColor="white" + style={ + Object { + "backgroundColor": undefined, + "color": "white", + "maxWidth": 0, + "minHeight": 0, + } + } + text={ + Object { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "text": "<p >Download</p>", + } + } + textAlign="center" + triggerKeyCodes={Array []} + /> + </View> + </View> + </View> +</View> +`; + +exports[`File block renders file without crashing 1`] = ` +<View + pointerEvents="box-none" +> + <View + accessible={true} + focusable={true} + onClick={[Function]} + onLayout={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + > + <Text + onTextLayout={[Function]} + style={ + Object { + "color": "gray", + } + } + > + + </Text> + Modal + <View> + <View> + <RCTAztecView + accessible={true} + activeFormats={Array []} + blockType={ + Object { + "tag": "p", + } + } + deleteEnter={true} + disableEditingMenu={false} + focusable={true} + fontFamily="serif" + isMultiline={false} + maxImagesWidth={200} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onSelectionChange={[Function]} + onStartShouldSetResponder={[Function]} + placeholder="File name" + placeholderTextColor="gray" + style={ + Object { + "backgroundColor": undefined, + "maxWidth": undefined, + "minHeight": 0, + } + } + text={ + Object { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "text": "<p >File name</p>", + } + } + textAlign="left" + triggerKeyCodes={Array []} + /> + </View> + </View> + <View + style={ + Array [ + Array [ + undefined, + false, + ], + Object { + "alignSelf": "flex-start", + }, + ] + } + > + <View> + <RCTAztecView + accessible={true} + activeFormats={Array []} + blockType={ + Object { + "tag": "p", + } + } + color="white" + deleteEnter={true} + disableEditingMenu={false} + focusable={true} + fontFamily="serif" + isMultiline={false} + maxImagesWidth={200} + minWidth={40} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onSelectionChange={[Function]} + onStartShouldSetResponder={[Function]} + placeholder="" + placeholderTextColor="white" + selectionColor="white" + style={ + Object { + "backgroundColor": undefined, + "color": "white", + "maxWidth": 0, + "minHeight": 0, + } + } + text={ + Object { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "text": "<p >Download</p>", + } + } + textAlign="center" + triggerKeyCodes={Array []} + /> + </View> + </View> + </View> +</View> +`; + +exports[`File block renders placeholder without crashing 1`] = ` +<View + style={ + Object { + "flex": 1, + } + } +> + <View + accessibilityHint="Double tap to select" + accessibilityLabel="File block. Empty" + accessibilityRole="button" + accessible={true} + focusable={true} + onClick={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + Array [ + Array [ + undefined, + undefined, + undefined, + ], + undefined, + ] + } + > + Modal + <View + style={ + Object { + "fill": "gray", + } + } + > + <View + style={Object {}} + > + Svg + </View> + </View> + <Text> + File + </Text> + <Text> + CHOOSE A FILE + </Text> + </View> +</View> +`; diff --git a/packages/block-library/src/file/test/edit.native.js b/packages/block-library/src/file/test/edit.native.js new file mode 100644 index 00000000000000..32a87adb25a9c3 --- /dev/null +++ b/packages/block-library/src/file/test/edit.native.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import renderer from 'react-test-renderer'; + +/** + * WordPress dependencies + */ +import { MediaUploadProgress } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { FileEdit } from '../edit.native.js'; + +const getTestComponentWithContent = ( attributes = {} ) => { + return renderer.create( + <FileEdit + attributes={ attributes } + setAttributes={ jest.fn() } + getMedia={ jest.fn() } + getStylesFromColorScheme={ jest.fn() } + /> + ); +}; + +describe( 'File block', () => { + it( 'renders placeholder without crashing', () => { + const component = getTestComponentWithContent(); + const rendered = component.toJSON(); + expect( rendered ).toMatchSnapshot(); + } ); + + it( 'renders file without crashing', () => { + const component = getTestComponentWithContent( { + showDownloadButton: true, + downloadButtonText: 'Download', + href: 'https://wordpress.org/latest.zip', + fileName: 'File name', + textLinkHref: 'https://wordpress.org/latest.zip', + id: '1', + } ); + + const rendered = component.toJSON(); + expect( rendered ).toMatchSnapshot(); + } ); + + it( 'renders file error state without crashing', () => { + const component = getTestComponentWithContent( { + showDownloadButton: true, + downloadButtonText: 'Download', + href: 'https://wordpress.org/latest.zip', + fileName: 'File name', + textLinkHref: 'https://wordpress.org/latest.zip', + id: '1', + } ); + + const mediaUpload = component.root.findByType( MediaUploadProgress ); + mediaUpload.instance.finishMediaUploadWithFailure( { mediaId: -1 } ); + + const rendered = component.toJSON(); + expect( rendered ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss index 2205dfdb1e52f1..c67ce5bf3778d2 100644 --- a/packages/block-library/src/gallery/editor.scss +++ b/packages/block-library/src/gallery/editor.scss @@ -7,16 +7,11 @@ } // @todo: this deserves a refactor, by being moved to the toolbar. - .block-editor-media-placeholder { - margin-bottom: $grid-unit-15; - padding: $grid-unit-15; - - // This element is empty here anyway. + .block-editor-media-placeholder.is-appender { .components-placeholder__label { display: none; } - - .components-button { + .block-editor-media-placeholder__button { margin-bottom: 0; } } diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index a8ead3186e057d..563f68b9541c70 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -106,6 +106,7 @@ figure.wp-block-image:not(.wp-block) { .wp-block-image__zoom { .components-popover__content { overflow: visible; + min-width: 260px; } .components-range-control { @@ -115,6 +116,8 @@ figure.wp-block-image:not(.wp-block) { .components-base-control__field { display: flex; margin-bottom: 0; + flex-direction: column; + align-items: flex-start; } } diff --git a/packages/block-library/src/image/image-editor.js b/packages/block-library/src/image/image-editor.js index a3401132390fb3..d0a8f4f8734d73 100644 --- a/packages/block-library/src/image/image-editor.js +++ b/packages/block-library/src/image/image-editor.js @@ -335,6 +335,7 @@ export default function ImageEditor( { ) } renderContent={ () => ( <RangeControl + label={ __( 'Zoom' ) } min={ MIN_ZOOM } max={ MAX_ZOOM } value={ Math.round( zoom ) } diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 66fa697b3eacd7..56a586f1aac250 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -50,7 +50,8 @@ "html": false, "inserter": true, "fontSize": true, - "__experimentalFontAppearance": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, "__experimentalTextTransform": true, "color": true, "__experimentalFontFamily": true, diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index 35a6650585f8cd..c85faf7a9dddf7 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -3,14 +3,27 @@ */ import { useEntityProp } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; -import { Icon, ToggleControl, PanelBody } from '@wordpress/components'; +import { + Icon, + ToggleControl, + PanelBody, + withNotices, +} from '@wordpress/components'; +import { + InspectorControls, + BlockControls, + MediaPlaceholder, + MediaReplaceFlow, + BlockIcon, + useBlockProps, +} from '@wordpress/block-editor'; import { __, sprintf } from '@wordpress/i18n'; -import { postFeaturedImage as icon } from '@wordpress/icons'; -import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; +import { postFeaturedImage } from '@wordpress/icons'; +const ALLOWED_MEDIA_TYPES = [ 'image' ]; const placeholderChip = ( <div className="post-featured-image_placeholder"> - <Icon icon={ icon } /> + <Icon icon={ postFeaturedImage } /> <p> { __( 'Featured Image' ) }</p> </div> ); @@ -19,8 +32,10 @@ function PostFeaturedImageDisplay( { attributes: { isLink }, setAttributes, context: { postId, postType }, + noticeUI, + noticeOperations, } ) { - const [ featuredImage ] = useEntityProp( + const [ featuredImage, setFeaturedImage ] = useEntityProp( 'postType', postType, 'featured_media', @@ -31,14 +46,44 @@ function PostFeaturedImageDisplay( { featuredImage && select( 'core' ).getMedia( featuredImage ), [ featuredImage ] ); - const image = ! media ? ( - placeholderChip - ) : ( - <img - src={ media.source_url } - alt={ media.alt_text || __( 'No alternative text set' ) } - /> - ); + const onSelectImage = ( value ) => { + if ( value?.id ) { + setFeaturedImage( value.id ); + } + }; + function onUploadError( message ) { + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( message ); + } + let image; + if ( ! featuredImage ) { + image = ( + <MediaPlaceholder + icon={ <BlockIcon icon={ postFeaturedImage } /> } + onSelect={ onSelectImage } + notices={ noticeUI } + onError={ onUploadError } + accept="image/*" + allowedTypes={ ALLOWED_MEDIA_TYPES } + labels={ { + title: __( 'Featured image' ), + instructions: __( + 'Upload a media file or pick one from your media library.' + ), + } } + /> + ); + } else { + // We have a Featured image so show a Placeholder if is loading. + image = ! media ? ( + placeholderChip + ) : ( + <img + src={ media.source_url } + alt={ media.alt_text || __( 'Featured image' ) } + /> + ); + } return ( <> @@ -55,14 +100,28 @@ function PostFeaturedImageDisplay( { /> </PanelBody> </InspectorControls> + <BlockControls> + { !! media && ( + <MediaReplaceFlow + mediaId={ featuredImage } + mediaURL={ media.source_url } + allowedTypes={ ALLOWED_MEDIA_TYPES } + accept="image/*" + onSelect={ onSelectImage } + onError={ onUploadError } + /> + ) } + </BlockControls> <div { ...useBlockProps() }>{ image }</div> </> ); } +const PostFeaturedImageWithNotices = withNotices( PostFeaturedImageDisplay ); + export default function PostFeaturedImageEdit( props ) { if ( ! props.context?.postId ) { return placeholderChip; } - return <PostFeaturedImageDisplay { ...props } />; + return <PostFeaturedImageWithNotices { ...props } />; } diff --git a/packages/block-library/src/post-featured-image/editor.scss b/packages/block-library/src/post-featured-image/editor.scss index 35faa55448736c..1f9cbb8e718a6e 100644 --- a/packages/block-library/src/post-featured-image/editor.scss +++ b/packages/block-library/src/post-featured-image/editor.scss @@ -1,4 +1,12 @@ div[data-type="core/post-featured-image"] { + img { + max-width: 100%; + height: auto; + display: block; + } +} + +.editor-styles-wrapper { .post-featured-image_placeholder { display: flex; flex-direction: row; @@ -16,9 +24,4 @@ div[data-type="core/post-featured-image"] { margin: 0; } } - img { - max-width: 100%; - height: auto; - display: block; - } } diff --git a/packages/block-library/src/post-title/edit.js b/packages/block-library/src/post-title/edit.js index 46030a4ed4cec3..e7c5b39f8cfaeb 100644 --- a/packages/block-library/src/post-title/edit.js +++ b/packages/block-library/src/post-title/edit.js @@ -6,12 +6,13 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { AlignmentToolbar, BlockControls, InspectorControls, useBlockProps, + PlainText, } from '@wordpress/block-editor'; import { ToolbarGroup, @@ -42,6 +43,7 @@ export default function PostTitleEdit( { ), [ postType, postId ] ); + const { editEntityRecord } = useDispatch( 'core' ); const blockProps = useBlockProps( { className: classnames( { @@ -53,11 +55,33 @@ export default function PostTitleEdit( { return null; } - let title = post.title || __( 'Post Title' ); + const { title, link } = post; + + let titleElement = ( + <PlainText + tagName={ TagName } + placeholder={ __( 'No Title' ) } + value={ title } + onChange={ ( value ) => + editEntityRecord( 'postType', postType, postId, { + title: value, + } ) + } + __experimentalVersion={ 2 } + { ...( isLink ? {} : blockProps ) } + /> + ); + if ( isLink ) { - title = ( - <a href={ post.link } target={ linkTarget } rel={ rel }> - { title } + titleElement = ( + <a + href={ link } + target={ linkTarget } + rel={ rel } + onClick={ ( event ) => event.preventDefault() } + { ...blockProps } + > + { titleElement } </a> ); } @@ -109,7 +133,7 @@ export default function PostTitleEdit( { ) } </PanelBody> </InspectorControls> - <TagName { ...blockProps }>{ title }</TagName> + { titleElement } </> ); } diff --git a/packages/block-library/src/query/variations.js b/packages/block-library/src/query/variations.js index 4d6d4f1913530a..dbf9290cc58fc1 100644 --- a/packages/block-library/src/query/variations.js +++ b/packages/block-library/src/query/variations.js @@ -41,7 +41,7 @@ const variations = [ }, { name: 'title-date', - title: __( 'Title and Date' ), + title: __( 'Title & Date' ), icon: titleDate, innerBlocks: [ [ @@ -54,7 +54,7 @@ const variations = [ }, { name: 'title-excerpt', - title: __( 'Title and Excerpt' ), + title: __( 'Title & Excerpt' ), icon: titleExcerpt, innerBlocks: [ [ @@ -67,7 +67,7 @@ const variations = [ }, { name: 'title-date-excerpt', - title: __( 'Title, Date and Excerpt' ), + title: __( 'Title, Date, & Excerpt' ), icon: titleDateExcerpt, innerBlocks: [ [ @@ -84,7 +84,7 @@ const variations = [ }, { name: 'image-date-title', - title: __( 'Image, Date and Title ' ), + title: __( 'Image, Date, & Title' ), icon: imageDateTitle, innerBlocks: [ [ diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index 7e7b1934b3fc31..040082c10c5f14 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -343,7 +343,6 @@ export default function SearchEdit( { width: `${ width }${ widthUnit }`, } } className="wp-block-search__inside-wrapper" - isResetValueOnUnitChange minWidth={ MIN_WIDTH } enable={ getResizableSides() } onResizeStart={ ( event, direction, elt ) => { diff --git a/packages/block-library/src/search/icons.js b/packages/block-library/src/search/icons.js index d795398382859b..3d50d161462965 100644 --- a/packages/block-library/src/search/icons.js +++ b/packages/block-library/src/search/icons.js @@ -18,7 +18,7 @@ export const buttonOutside = ( height="9.5" transform="rotate(-90 4.75 15.25)" stroke="currentColor" - stroke-width="1.5" + strokeWidth="1.5" fill="none" /> <Rect x="16" y="10" width="4" height="4" rx="1" fill="currentColor" /> @@ -34,7 +34,7 @@ export const buttonInside = ( height="14.5" transform="rotate(-90 4.75 15.25)" stroke="currentColor" - stroke-width="1.5" + strokeWidth="1.5" fill="none" /> <Rect x="14" y="10" width="4" height="4" rx="1" fill="currentColor" /> @@ -51,7 +51,7 @@ export const noButton = ( transform="rotate(-90 4.75 15.25)" stroke="currentColor" fill="none" - stroke-width="1.5" + strokeWidth="1.5" /> </SVG> ); @@ -66,7 +66,7 @@ export const buttonWithIcon = ( rx="1.25" stroke="currentColor" fill="none" - stroke-width="1.5" + strokeWidth="1.5" /> <Rect x="8" y="11" width="8" height="2" fill="currentColor" /> </SVG> @@ -82,7 +82,7 @@ export const toggleLabel = ( transform="rotate(-90 4.75 17.25)" stroke="currentColor" fill="none" - stroke-width="1.5" + strokeWidth="1.5" /> <Rect x="4" y="7" width="10" height="2" fill="currentColor" /> </SVG> diff --git a/packages/block-library/src/template-part/index.php b/packages/block-library/src/template-part/index.php index c19f23c7bd546b..a960aa969bc906 100644 --- a/packages/block-library/src/template-part/index.php +++ b/packages/block-library/src/template-part/index.php @@ -45,7 +45,7 @@ function render_block_core_template_part( $attributes ) { // Else, if the template part was provided by the active theme, // render the corresponding file content. $template_part_file_path = get_stylesheet_directory() . '/block-template-parts/' . $attributes['slug'] . '.html'; - if ( 0 === validate_file( $template_part_file_path ) && file_exists( $template_part_file_path ) ) { + if ( 0 === validate_file( $attributes['slug'] ) && file_exists( $template_part_file_path ) ) { $content = file_get_contents( $template_part_file_path ); } } diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 1dee92f5b47b08..68a08552f16b09 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -39,11 +39,11 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, fontStyle: { value: [ 'typography', 'fontStyle' ], - support: [ '__experimentalFontAppearance' ], + support: [ '__experimentalFontStyle' ], }, fontWeight: { value: [ 'typography', 'fontWeight' ], - support: [ '__experimentalFontAppearance' ], + support: [ '__experimentalFontWeight' ], }, lineHeight: { value: [ 'typography', 'lineHeight' ], diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index 2b081bf471fbb4..86b0147beea13a 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -17,6 +17,7 @@ import { getBlockType, getFreeformContentHandlerName, getUnregisteredTypeHandlerName, + hasBlockSupport, } from './registration'; import { normalizeBlockType } from './utils'; import BlockContentProvider from '../block-content-provider'; @@ -116,10 +117,14 @@ export function getSaveElement( let element = save( { attributes, innerBlocks } ); + const hasLightBlockWrapper = + blockType.apiVersion > 1 || + hasBlockSupport( blockType, 'lightBlockWrapper', false ); + if ( isObject( element ) && hasFilter( 'blocks.getSaveContent.extraProps' ) && - ! blockType.apiVersion + ! hasLightBlockWrapper ) { /** * Filters the props applied to the block save result element. diff --git a/packages/components/src/color-edit/index.js b/packages/components/src/color-edit/index.js index a22167efc94858..31b49ad1454b33 100644 --- a/packages/components/src/color-edit/index.js +++ b/packages/components/src/color-edit/index.js @@ -53,7 +53,8 @@ function ColorOption( { ); const isShowingControls = - isHover || isFocused || isEditingName || isShowingAdvancedPanel; + ( isHover || isFocused || isEditingName || isShowingAdvancedPanel ) && + ! immutableColorSlugs.includes( slug ); return ( <div @@ -119,9 +120,7 @@ function ColorOption( { onChange={ ( newColorName ) => onChange( { color, - slug: immutableColorSlugs.includes( slug ) - ? slug - : kebabCase( newColorName ), + slug: kebabCase( newColorName ), name: newColorName, } ) } diff --git a/packages/components/src/color-palette/index.native.js b/packages/components/src/color-palette/index.native.js index cc75a22c67d3c8..8df4889acd83b0 100644 --- a/packages/components/src/color-palette/index.native.js +++ b/packages/components/src/color-palette/index.native.js @@ -46,9 +46,9 @@ function ColorPalette( { customIndicatorWrapperStyles, } ) { const customSwatchGradients = [ - 'linear-gradient(120deg, rgba(255,0,0,.8), 0%, rgba(255,255,255,1) 70.71%)', - 'linear-gradient(240deg, rgba(0,255,0,.8), 0%, rgba(0,255,0,0) 70.71%)', - 'linear-gradient(360deg, rgba(0,0,255,.8), 0%, rgba(0,0,255,0) 70.71%)', + 'linear-gradient(120deg, rgba(255,0,0,.8) 0%, rgba(255,255,255,1) 70.71%)', + 'linear-gradient(240deg, rgba(0,255,0,.8) 0%, rgba(0,255,0,0) 70.71%)', + 'linear-gradient(360deg, rgba(0,0,255,.8) 0%, rgba(0,0,255,0) 70.71%)', ]; const scrollViewRef = useRef(); diff --git a/packages/components/src/custom-gradient-picker/serializer.js b/packages/components/src/custom-gradient-picker/serializer.js index 90166311ce0b1d..c270ac50544cd7 100644 --- a/packages/components/src/custom-gradient-picker/serializer.js +++ b/packages/components/src/custom-gradient-picker/serializer.js @@ -13,7 +13,11 @@ export function serializeGradientColor( { type, value } ) { return `${ type }(${ value.join( ',' ) })`; } -export function serializeGradientPosition( { type, value } ) { +export function serializeGradientPosition( position ) { + if ( ! position ) { + return ''; + } + const { value, type } = position; return `${ value }${ type }`; } diff --git a/packages/components/src/date-time/test/time.js b/packages/components/src/date-time/test/time.js index 32a47ca9ed2dc6..7124e80e6c1b1a 100644 --- a/packages/components/src/date-time/test/time.js +++ b/packages/components/src/date-time/test/time.js @@ -271,4 +271,28 @@ describe( 'TimePicker', () => { expect( monthInputIndex > dayInputIndex ).toBe( true ); } ); + + it( 'Should set a time when passed a null currentTime', () => { + const onChangeSpy = jest.fn(); + + render( + <TimePicker + currentTime={ null } + onChange={ onChangeSpy } + is12Hour + /> + ); + + const monthInput = screen.getByLabelText( 'Month' ).value; + const dayInput = screen.getByLabelText( 'Day' ).value; + const yearInput = screen.getByLabelText( 'Year' ).value; + const hoursInput = screen.getByLabelText( 'Hours' ).value; + const minutesInput = screen.getByLabelText( 'Minutes' ).value; + + expect( Number.isNaN( parseInt( monthInput, 10 ) ) ).toBe( false ); + expect( Number.isNaN( parseInt( dayInput, 10 ) ) ).toBe( false ); + expect( Number.isNaN( parseInt( yearInput, 10 ) ) ).toBe( false ); + expect( Number.isNaN( parseInt( hoursInput, 10 ) ) ).toBe( false ); + expect( Number.isNaN( parseInt( minutesInput, 10 ) ) ).toBe( false ); + } ); } ); diff --git a/packages/components/src/date-time/time.js b/packages/components/src/date-time/time.js index 5df865e95e27d4..de10573f992524 100644 --- a/packages/components/src/date-time/time.js +++ b/packages/components/src/date-time/time.js @@ -88,7 +88,9 @@ export function TimePicker( { is12Hour, currentTime, onChange } ) { // Reset the state when currentTime changed. useEffect( () => { - setDate( moment( currentTime ).startOf( 'minutes' ) ); + setDate( + currentTime ? moment( currentTime ).startOf( 'minutes' ) : moment() + ); }, [ currentTime ] ); const { day, month, year, minutes, hours, am } = useMemo( diff --git a/packages/components/src/drop-zone/provider.js b/packages/components/src/drop-zone/provider.js index 45c6954a6a9be5..80faacf92102cf 100644 --- a/packages/components/src/drop-zone/provider.js +++ b/packages/components/src/drop-zone/provider.js @@ -22,7 +22,7 @@ const { Provider } = Context; function getDragEventType( { dataTransfer } ) { if ( dataTransfer ) { - if ( getFilesFromDataTransfer( dataTransfer ).size > 0 ) { + if ( getFilesFromDataTransfer( dataTransfer ).length > 0 ) { return 'file'; } @@ -204,7 +204,7 @@ export default function DropZoneProvider( { children } ) { switch ( dragEventType ) { case 'file': hoveredDropZone.onFilesDrop( - [ ...getFilesFromDataTransfer( event.dataTransfer ) ], + getFilesFromDataTransfer( event.dataTransfer ), position ); break; diff --git a/packages/components/src/mobile/bottom-sheet/range-cell.native.js b/packages/components/src/mobile/bottom-sheet/range-cell.native.js index da50c1206a31e6..b7a37c66b9b61c 100644 --- a/packages/components/src/mobile/bottom-sheet/range-cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/range-cell.native.js @@ -34,7 +34,7 @@ class BottomSheetRangeCell extends Component { this.onCellPress = this.onCellPress.bind( this ); const { value, defaultValue, minimumValue } = props; - const initialValue = value || defaultValue || minimumValue; + const initialValue = Number( value || defaultValue || minimumValue ); this.state = { accessible: true, diff --git a/packages/components/src/mobile/gradient/index.native.js b/packages/components/src/mobile/gradient/index.native.js index ed0e0b76664104..6974093544e967 100644 --- a/packages/components/src/mobile/gradient/index.native.js +++ b/packages/components/src/mobile/gradient/index.native.js @@ -3,6 +3,7 @@ */ import { View, Platform } from 'react-native'; import RNLinearGradient from 'react-native-linear-gradient'; +import gradientParser from 'gradient-parser'; /** * WordPress dependencies */ @@ -16,17 +17,72 @@ import { useResizeObserver } from '@wordpress/compose'; import styles from './style.scss'; function getGradientAngle( gradientValue ) { - const matchDeg = /(\d+)deg/g; + const matchAngle = /\(((\d+deg)|(to\s[^,]+))/; + const angle = matchAngle.exec( gradientValue )[ 1 ]; + const angleBase = 45; - return Number( matchDeg.exec( gradientValue )[ 1 ] ); + const angleType = angle.includes( 'deg' ) ? 'angle' : 'sideOrCorner'; + + if ( angleType === 'sideOrCorner' ) { + switch ( angle ) { + case 'to top': + return 0; + case 'to top right': + case 'to right top': + return angleBase; + case 'to right': + return 2 * angleBase; + case 'to bottom right': + case 'to right bottom': + return 3 * angleBase; + case 'to bottom': + return 4 * angleBase; + case 'to bottom left': + case 'to left bottom': + return 5 * angleBase; + case 'to left': + return 6 * angleBase; + case 'to top left': + case 'to left top': + return 7 * angleBase; + } + } else if ( angleType === 'angle' ) { + return parseFloat( angle ); + } else return 180; } function getGradientColorGroup( gradientValue ) { - const matchColorGroup = /(rgba|rgb|#)(.+?)[\%]/g; + const colorNeedParenthesis = [ 'rgb', 'rgba' ]; + + const excludeSideOrCorner = /linear-gradient\(to\s+([a-z\s]+,)/; + + // Parser has some difficulties with angle defined as a side or corner (e.g. `to left`) + // so it's going to be excluded in order to matching color groups + const modifiedGradientValue = gradientValue.replace( + excludeSideOrCorner, + 'linear-gradient(' + ); + + return [].concat( + ...gradientParser.parse( modifiedGradientValue )?.map( ( gradient ) => + gradient.colorStops?.map( ( color, index ) => { + const { type, value, length } = color; + const fallbackLength = `${ + 100 * ( index / ( gradient.colorStops.length - 1 ) ) + }%`; + const colorLength = length + ? `${ length.value }${ length.type }` + : fallbackLength; - return gradientValue - .match( matchColorGroup ) - .map( ( color ) => color.split( ' ' ) ); + if ( colorNeedParenthesis.includes( type ) ) { + return [ `${ type }(${ value.join( ',' ) })`, colorLength ]; + } else if ( type === 'literal' ) { + return [ value, colorLength ]; + } + return [ `#${ value }`, colorLength ]; + } ) + ) + ); } function getGradientBaseColors( gradientValue ) { diff --git a/packages/components/src/modal/style.scss b/packages/components/src/modal/style.scss index eecdbbbd4558a9..6c4072c098c5ba 100644 --- a/packages/components/src/modal/style.scss +++ b/packages/components/src/modal/style.scss @@ -5,7 +5,7 @@ right: 0; bottom: 0; left: 0; - background-color: rgba($black, 0.7); + background-color: rgba($black, 0.35); z-index: z-index(".components-modal__screen-overlay"); // This animates the appearance of the white background. diff --git a/packages/components/src/navigation/menu/index.js b/packages/components/src/navigation/menu/index.js index 4eadb9d05a8e2b..0890becbfb26cf 100644 --- a/packages/components/src/navigation/menu/index.js +++ b/packages/components/src/navigation/menu/index.js @@ -17,6 +17,7 @@ import { useNavigationContext } from '../context'; import { useNavigationTreeMenu } from './use-navigation-tree-menu'; import NavigationBackButton from '../back-button'; import NavigationMenuTitle from './menu-title'; +import NavigationSearchNoResultsFound from './search-no-results-found'; import { NavigableMenu } from '../../navigable-container'; import { MenuUI } from '../styles/navigation-styles'; @@ -81,7 +82,12 @@ export default function NavigationMenu( props ) { /> <NavigableMenu> - <ul aria-labelledby={ menuTitleId }>{ children }</ul> + <ul aria-labelledby={ menuTitleId }> + { children } + { search && ( + <NavigationSearchNoResultsFound search={ search } /> + ) } + </ul> </NavigableMenu> </MenuUI> </NavigationMenuContext.Provider> diff --git a/packages/components/src/navigation/menu/search-no-results-found.js b/packages/components/src/navigation/menu/search-no-results-found.js new file mode 100644 index 00000000000000..07391714c8dff3 --- /dev/null +++ b/packages/components/src/navigation/menu/search-no-results-found.js @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { filter } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useNavigationContext } from '../context'; +import { ItemBaseUI, ItemUI } from '../styles/navigation-styles'; + +export default function NavigationSearchNoResultsFound( { search } ) { + const { + navigationTree: { items }, + } = useNavigationContext(); + + const resultsCount = filter( items, '_isVisible' ).length; + + if ( ! search || !! resultsCount ) { + return null; + } + + return ( + <ItemBaseUI> + <ItemUI>{ __( 'No results found.' ) } </ItemUI> + </ItemBaseUI> + ); +} diff --git a/packages/components/src/range-control/styles/range-control-styles.js b/packages/components/src/range-control/styles/range-control-styles.js index 4f49803f81d945..2b4d0f94633670 100644 --- a/packages/components/src/range-control/styles/range-control-styles.js +++ b/packages/components/src/range-control/styles/range-control-styles.js @@ -13,7 +13,7 @@ import { color, reduceMotion, rtl, space } from '../../utils/style-mixins'; const rangeHeight = () => css( { height: 30, minHeight: 30 } ); const thumbSize = 20; -export const Root = styled.span` +export const Root = styled.div` -webkit-tap-highlight-color: transparent; box-sizing: border-box; align-items: flex-start; @@ -31,7 +31,7 @@ const wrapperColor = ( { color: colorProp = color( 'ui.borderFocus' ) } ) => { const wrapperMargin = ( { marks } ) => css( { marginBottom: marks ? 16 : null } ); -export const Wrapper = styled.span` +export const Wrapper = styled.div` box-sizing: border-box; color: ${ color( 'blue.medium.focus' ) }; display: block; diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 11cd09d9cdab47..f1443e842e4bf4 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -13,6 +13,7 @@ import { useCallback, useEffect, useReducer, + useMemo, } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; @@ -94,6 +95,23 @@ export default function useSelect( _mapSelect, deps ) { const latestMapOutputError = useRef(); const isMountedAndNotUnsubscribing = useRef(); + // Keep track of the stores being selected in the mapSelect function, + // and only subscribe to those stores later. + const listeningStores = useRef( [] ); + const trapSelect = useCallback( + ( callback ) => + registry.__experimentalMarkListeningStores( + callback, + listeningStores + ), + [ registry ] + ); + + // Generate a "flag" for used in the effect dependency array. + // It's different than just using `mapSelect` since deps could be undefined, + // in that case, we would still want to memoize it. + const depsChangedFlag = useMemo( () => ( {} ), deps || [] ); + let mapOutput; try { @@ -101,7 +119,9 @@ export default function useSelect( _mapSelect, deps ) { latestMapSelect.current !== mapSelect || latestMapOutputError.current ) { - mapOutput = mapSelect( registry.select, registry ); + mapOutput = trapSelect( () => + mapSelect( registry.select, registry ) + ); } else { mapOutput = latestMapOutput.current; } @@ -140,10 +160,10 @@ export default function useSelect( _mapSelect, deps ) { const onStoreChange = () => { if ( isMountedAndNotUnsubscribing.current ) { try { - const newMapOutput = latestMapSelect.current( - registry.select, - registry + const newMapOutput = trapSelect( () => + latestMapSelect.current( registry.select, registry ) ); + if ( isShallowEqual( latestMapOutput.current, newMapOutput ) ) { @@ -165,20 +185,24 @@ export default function useSelect( _mapSelect, deps ) { onStoreChange(); } - const unsubscribe = registry.subscribe( () => { + const onChange = () => { if ( latestIsAsync.current ) { renderQueue.add( queueContext, onStoreChange ); } else { onStoreChange(); } - } ); + }; + + const unsubscribers = listeningStores.current.map( ( storeName ) => + registry.__experimentalSubscribeStore( storeName, onChange ) + ); return () => { isMountedAndNotUnsubscribing.current = false; - unsubscribe(); + unsubscribers.forEach( ( unsubscribe ) => unsubscribe() ); renderQueue.flush( queueContext ); }; - }, [ registry ] ); + }, [ registry, trapSelect, depsChangedFlag ] ); return mapOutput; } diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 78a81ba3d47986..d5415191ffcddf 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -3,10 +3,16 @@ */ import TestRenderer, { act } from 'react-test-renderer'; +/** + * WordPress dependencies + */ +import { useState, useReducer } from '@wordpress/element'; + /** * Internal dependencies */ import { createRegistry } from '../../../registry'; +import { createRegistrySelector } from '../../../factory'; import { RegistryProvider } from '../../registry-provider'; import useSelect from '../index'; @@ -110,7 +116,6 @@ describe( 'useSelect', () => { } ); // rerender with dependency changed - // rerender with non dependency changed act( () => { renderer.update( <RegistryProvider value={ registry }> @@ -120,7 +125,7 @@ describe( 'useSelect', () => { } ); expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); - expect( selectSpyBar ).toHaveBeenCalledTimes( 1 ); + expect( selectSpyBar ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); // ensure expected state was rendered @@ -133,23 +138,22 @@ describe( 'useSelect', () => { const data = useSelect( mapSelectSpy, [] ); return <div data={ data } />; }; - let subscribedSpy, TestComponent; + let TestComponent; const mapSelectSpy = jest.fn( ( select ) => select( 'testStore' ).testSelector() ); const selectorSpy = jest.fn(); - const subscribeCallback = ( subscription ) => { - subscribedSpy = subscription; - }; beforeEach( () => { registry.registerStore( 'testStore', { - reducer: () => null, + actions: { + forceUpdate: () => ( { type: 'FORCE_UPDATE' } ), + }, + reducer: ( state = {} ) => ( { ...state } ), selectors: { testSelector: selectorSpy, }, } ); - registry.subscribe = subscribeCallback; TestComponent = getComponent( mapSelectSpy ); } ); afterEach( () => { @@ -194,7 +198,7 @@ describe( 'useSelect', () => { // subscription which should in turn trigger a re-render. act( () => { selectorSpy.mockReturnValue( valueB ); - subscribedSpy(); + registry.dispatch( 'testStore' ).forceUpdate(); } ); expect( testInstance.findByType( 'div' ).props.data ).toEqual( valueB @@ -203,4 +207,363 @@ describe( 'useSelect', () => { } ); } ); + + describe( 're-calls the selector as minimal times as possible', () => { + const counterStore = { + actions: { + increment: () => ( { type: 'INCREMENT' } ), + }, + reducer: ( state, action ) => { + if ( ! state ) { + return { counter: 0 }; + } + if ( action?.type === 'INCREMENT' ) { + return { counter: state.counter + 1 }; + } + return state; + }, + selectors: { + getCounter: ( state ) => state.counter, + }, + }; + + it( 'only calls the selectors it has selected', () => { + registry.registerStore( 'store-1', counterStore ); + registry.registerStore( 'store-2', counterStore ); + + let renderer; + + const selectCount1 = jest.fn(); + const selectCount2 = jest.fn(); + + const TestComponent = jest.fn( () => { + const count1 = useSelect( + ( select ) => + selectCount1() || select( 'store-1' ).getCounter(), + [] + ); + useSelect( + ( select ) => + selectCount2() || select( 'store-2' ).getCounter(), + [] + ); + + return <div data={ count1 } />; + } ); + + act( () => { + renderer = TestRenderer.create( + <RegistryProvider value={ registry }> + <TestComponent /> + </RegistryProvider> + ); + } ); + + const testInstance = renderer.root; + + expect( selectCount1 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); + expect( TestComponent ).toHaveBeenCalledTimes( 1 ); + expect( testInstance.findByType( 'div' ).props.data ).toBe( 0 ); + + act( () => { + registry.dispatch( 'store-2' ).increment(); + } ); + + expect( selectCount1 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 3 ); + expect( TestComponent ).toHaveBeenCalledTimes( 2 ); + expect( testInstance.findByType( 'div' ).props.data ).toBe( 0 ); + + act( () => { + registry.dispatch( 'store-1' ).increment(); + } ); + + expect( selectCount1 ).toHaveBeenCalledTimes( 3 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 3 ); + expect( TestComponent ).toHaveBeenCalledTimes( 3 ); + expect( testInstance.findByType( 'div' ).props.data ).toBe( 1 ); + } ); + + it( 'can subscribe to multiple stores at once', () => { + registry.registerStore( 'store-1', counterStore ); + registry.registerStore( 'store-2', counterStore ); + registry.registerStore( 'store-3', counterStore ); + + let renderer; + + const selectCount1And2 = jest.fn(); + + const TestComponent = jest.fn( () => { + const { count1, count2 } = useSelect( + ( select ) => + selectCount1And2() || { + count1: select( 'store-1' ).getCounter(), + count2: select( 'store-2' ).getCounter(), + }, + [] + ); + + return <div data={ { count1, count2 } } />; + } ); + + act( () => { + renderer = TestRenderer.create( + <RegistryProvider value={ registry }> + <TestComponent /> + </RegistryProvider> + ); + } ); + + const testInstance = renderer.root; + + expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 ); + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 0, + count2: 0, + } ); + + act( () => { + registry.dispatch( 'store-2' ).increment(); + } ); + + expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 ); + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 0, + count2: 1, + } ); + + act( () => { + registry.dispatch( 'store-3' ).increment(); + } ); + + expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 ); + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 0, + count2: 1, + } ); + } ); + + it( 're-calls the selector when deps changed', () => { + registry.registerStore( 'store-1', counterStore ); + registry.registerStore( 'store-2', counterStore ); + registry.registerStore( 'store-3', counterStore ); + + let renderer, dep, setDep; + const selectCount1AndDep = jest.fn(); + + const TestComponent = jest.fn( () => { + [ dep, setDep ] = useState( 0 ); + const state = useSelect( + ( select ) => + selectCount1AndDep() || { + count1: select( 'store-1' ).getCounter(), + dep, + }, + [ dep ] + ); + + return <div data={ state } />; + } ); + + act( () => { + renderer = TestRenderer.create( + <RegistryProvider value={ registry }> + <TestComponent /> + </RegistryProvider> + ); + } ); + + const testInstance = renderer.root; + + expect( selectCount1AndDep ).toHaveBeenCalledTimes( 2 ); + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 0, + dep: 0, + } ); + + act( () => { + setDep( 1 ); + } ); + + expect( selectCount1AndDep ).toHaveBeenCalledTimes( 4 ); + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 0, + dep: 1, + } ); + + act( () => { + registry.dispatch( 'store-1' ).increment(); + } ); + + expect( selectCount1AndDep ).toHaveBeenCalledTimes( 5 ); + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 1, + dep: 1, + } ); + } ); + + it( 'handles registry selectors', () => { + const getCount1And2 = createRegistrySelector( + ( select ) => ( state ) => ( { + count1: state.counter, + count2: select( 'store-2' ).getCounter(), + } ) + ); + + registry.registerStore( 'store-1', { + ...counterStore, + selectors: { + ...counterStore.selectors, + getCount1And2, + }, + } ); + registry.registerStore( 'store-2', counterStore ); + + let renderer; + const selectCount1And2 = jest.fn(); + + const TestComponent = jest.fn( () => { + const state = useSelect( + ( select ) => + selectCount1And2() || + select( 'store-1' ).getCount1And2(), + [] + ); + + return <div data={ state } />; + } ); + + act( () => { + renderer = TestRenderer.create( + <RegistryProvider value={ registry }> + <TestComponent /> + </RegistryProvider> + ); + } ); + + const testInstance = renderer.root; + + expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 ); + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 0, + count2: 0, + } ); + + act( () => { + registry.dispatch( 'store-2' ).increment(); + } ); + + expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 ); + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + count1: 0, + count2: 1, + } ); + } ); + + it( 'handles conditional statements in selectors', () => { + registry.registerStore( 'store-1', counterStore ); + registry.registerStore( 'store-2', counterStore ); + + let renderer, shouldSelectCount1, toggle; + const selectCount1 = jest.fn(); + const selectCount2 = jest.fn(); + + const TestComponent = jest.fn( () => { + [ shouldSelectCount1, toggle ] = useReducer( + ( should ) => ! should, + false + ); + const state = useSelect( + ( select ) => { + if ( shouldSelectCount1 ) { + selectCount1(); + select( 'store-1' ).getCounter(); + return 'count1'; + } + + selectCount2(); + select( 'store-2' ).getCounter(); + return 'count2'; + }, + [ shouldSelectCount1 ] + ); + + return <div data={ state } />; + } ); + + act( () => { + renderer = TestRenderer.create( + <RegistryProvider value={ registry }> + <TestComponent /> + </RegistryProvider> + ); + } ); + + const testInstance = renderer.root; + + expect( selectCount1 ).toHaveBeenCalledTimes( 0 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); + expect( testInstance.findByType( 'div' ).props.data ).toBe( + 'count2' + ); + + act( () => { + toggle(); + } ); + + expect( selectCount1 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); + expect( testInstance.findByType( 'div' ).props.data ).toBe( + 'count1' + ); + } ); + + it( "handles subscriptions to the parent's stores", () => { + registry.registerStore( 'parent-store', counterStore ); + + const subRegistry = createRegistry( {}, registry ); + subRegistry.registerStore( 'child-store', counterStore ); + + let renderer; + + const TestComponent = jest.fn( () => { + const state = useSelect( + ( select ) => ( { + parentCount: select( 'parent-store' ).getCounter(), + childCount: select( 'child-store' ).getCounter(), + } ), + [] + ); + + return <div data={ state } />; + } ); + + act( () => { + renderer = TestRenderer.create( + <RegistryProvider value={ registry }> + <RegistryProvider value={ subRegistry }> + <TestComponent /> + </RegistryProvider> + </RegistryProvider> + ); + } ); + + const testInstance = renderer.root; + + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + parentCount: 0, + childCount: 0, + } ); + + act( () => { + registry.dispatch( 'parent-store' ).increment(); + } ); + + expect( testInstance.findByType( 'div' ).props.data ).toEqual( { + parentCount: 1, + childCount: 0, + } ); + } ); + } ); } ); diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index 26f521c99059be..efb094b0b3159c 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -666,11 +666,7 @@ describe( 'withSelect', () => { registry.dispatch( 'childRender' ).toggleRender(); } ); - // 3 times because - // - 1 on initial render - // - 1 on effect before subscription set. - // - 1 child subscription fires. - expect( childMapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 2 ); diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index 55117182ba78c9..edfd3083712ccb 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -136,7 +136,7 @@ export default function createReduxStore( key, options ) { store && ( ( listener ) => { let lastState = store.__unstableOriginalGetState(); - store.subscribe( () => { + return store.subscribe( () => { const state = store.__unstableOriginalGetState(); const hasChanged = state !== lastState; lastState = state; diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 674227f1314091..9924896ef04d89 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -50,6 +50,7 @@ import createCoreDataStore from './store'; export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; let listeners = []; + const __experimentalListeningStores = new Set(); /** * Global listener called for each store's update. @@ -85,6 +86,7 @@ export function createRegistry( storeConfigs = {}, parent = null ) { const storeName = isObject( storeNameOrDefinition ) ? storeNameOrDefinition.name : storeNameOrDefinition; + __experimentalListeningStores.add( storeName ); const store = stores[ storeName ]; if ( store ) { return store.getSelectors(); @@ -93,6 +95,13 @@ export function createRegistry( storeConfigs = {}, parent = null ) { return parent && parent.select( storeName ); } + function __experimentalMarkListeningStores( callback, ref ) { + __experimentalListeningStores.clear(); + const result = callback.call( this ); + ref.current = Array.from( __experimentalListeningStores ); + return result; + } + const getResolveSelectors = memize( ( selectors ) => { return mapValues( @@ -213,6 +222,21 @@ export function createRegistry( storeConfigs = {}, parent = null ) { registerGenericStore( store.name, store.instantiate( registry ) ); } + /** + * Subscribe handler to a store. + * + * @param {string[]} storeName The store name. + * @param {Function} handler The function subscribed to the store. + * @return {Function} A function to unsubscribe the handler. + */ + function __experimentalSubscribeStore( storeName, handler ) { + if ( storeName in stores ) { + return stores[ storeName ].subscribe( handler ); + } + + return parent.__experimentalSubscribeStore( storeName, handler ); + } + let registry = { registerGenericStore, stores, @@ -223,6 +247,8 @@ export function createRegistry( storeConfigs = {}, parent = null ) { dispatch, use, register, + __experimentalMarkListeningStores, + __experimentalSubscribeStore, }; /** diff --git a/packages/dom/README.md b/packages/dom/README.md index 6fe8d1ca75825d..d888a810f109d9 100644 --- a/packages/dom/README.md +++ b/packages/dom/README.md @@ -84,7 +84,7 @@ _Parameters_ _Returns_ -- `Set`: A set containing all files. +- `Array<Object>`: An array containing all files. <a name="getOffsetParent" href="#getOffsetParent">#</a> **getOffsetParent** diff --git a/packages/dom/src/data-transfer.js b/packages/dom/src/data-transfer.js index 48cd3c55d9d41c..6052326c2c2213 100644 --- a/packages/dom/src/data-transfer.js +++ b/packages/dom/src/data-transfer.js @@ -3,16 +3,24 @@ * * @param {DataTransfer} dataTransfer DataTransfer object to inspect. * - * @return {Set} A set containing all files. + * @return {Object[]} An array containing all files. */ export function getFilesFromDataTransfer( dataTransfer ) { - const files = new Set( dataTransfer.files ); + const files = [ ...dataTransfer.files ]; Array.from( dataTransfer.items ).forEach( ( item ) => { const file = item.getAsFile(); - if ( file ) { - files.add( file ); + if ( + file && + ! files.find( + ( { name, type, size } ) => + name === file.name && + type === file.type && + size === file.size + ) + ) { + files.push( file ); } } ); diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index 2bb8f370870dc6..e3b052a81f4dbd 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -536,6 +536,17 @@ _Parameters_ - _viewport_ `WPViewport`: Viewport name or dimensions object to assign. +<a name="setClipboardData" href="#setClipboardData">#</a> **setClipboardData** + +Sets the clipboard data that can be pasted with +`pressKeyWithModifier( 'primary', 'v' )`. + +_Parameters_ + +- _$1_ `Object`: Options. +- _$1.plainText_ `string`: Plain text to set. +- _$1.html_ `string`: HTML to set. + <a name="setPostContent" href="#setPostContent">#</a> **setPostContent** Sets code editor content diff --git a/packages/e2e-test-utils/src/click-block-toolbar-button.js b/packages/e2e-test-utils/src/click-block-toolbar-button.js index dc27849e36d5e6..3aa431bfe29ff9 100644 --- a/packages/e2e-test-utils/src/click-block-toolbar-button.js +++ b/packages/e2e-test-utils/src/click-block-toolbar-button.js @@ -15,12 +15,13 @@ export async function clickBlockToolbarButton( label, type = 'ariaLabel' ) { let button; if ( type === 'ariaLabel' ) { - const BUTTON_SELECTOR = `.${ BLOCK_TOOLBAR_SELECTOR } button[aria-label="${ label }"]`; - button = await page.waitForSelector( BUTTON_SELECTOR ); + button = await page.waitForSelector( + `.${ BLOCK_TOOLBAR_SELECTOR } button[aria-label="${ label }"]` + ); } if ( type === 'content' ) { - [ button ] = await page.$x( + button = await page.waitForXPath( `//*[@class='${ BLOCK_TOOLBAR_SELECTOR }']//button[contains(text(), '${ label }')]` ); } diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js index 2c822577d683ca..39660c8173c223 100644 --- a/packages/e2e-test-utils/src/index.js +++ b/packages/e2e-test-utils/src/index.js @@ -52,7 +52,10 @@ export { openDocumentSettingsSidebar } from './open-document-settings-sidebar'; export { openPublishPanel } from './open-publish-panel'; export { trashAllPosts } from './posts'; export { pressKeyTimes } from './press-key-times'; -export { pressKeyWithModifier } from './press-key-with-modifier'; +export { + pressKeyWithModifier, + setClipboardData, +} from './press-key-with-modifier'; export { publishPost } from './publish-post'; export { publishPostWithPrePublishChecksDisabled } from './publish-post-with-pre-publish-checks-disabled'; export { saveDraft } from './save-draft'; diff --git a/packages/e2e-test-utils/src/press-key-with-modifier.js b/packages/e2e-test-utils/src/press-key-with-modifier.js index abb7f168e60901..b79f27c1d4a72b 100644 --- a/packages/e2e-test-utils/src/press-key-with-modifier.js +++ b/packages/e2e-test-utils/src/press-key-with-modifier.js @@ -81,6 +81,26 @@ async function emulateSelectAll() { } ); } +/** + * Sets the clipboard data that can be pasted with + * `pressKeyWithModifier( 'primary', 'v' )`. + * + * @param {Object} $1 Options. + * @param {string} $1.plainText Plain text to set. + * @param {string} $1.html HTML to set. + */ +export async function setClipboardData( { plainText = '', html = '' } ) { + await page.evaluate( + ( _plainText, _html ) => { + window._clipboardData = new DataTransfer(); + window._clipboardData.setData( 'text/plain', _plainText ); + window._clipboardData.setData( 'text/html', _html ); + }, + plainText, + html + ); +} + async function emulateClipboard( type ) { await page.evaluate( ( _type ) => { if ( _type !== 'paste' ) { diff --git a/packages/e2e-test-utils/src/shared/get-json-response.js b/packages/e2e-test-utils/src/shared/get-json-response.js index 4d8581129de799..c09d6f3a186624 100644 --- a/packages/e2e-test-utils/src/shared/get-json-response.js +++ b/packages/e2e-test-utils/src/shared/get-json-response.js @@ -6,7 +6,7 @@ */ export function getJSONResponse( obj ) { return { - content: 'application/json', + contentType: 'application/json', body: JSON.stringify( obj ), }; } diff --git a/packages/e2e-tests/assets/10x10_e2e_test_image_z9T8jK.png b/packages/e2e-tests/assets/10x10_e2e_test_image_z9T8jK.png index 4d198c0023578e..a13b8d3415a5a9 100644 Binary files a/packages/e2e-tests/assets/10x10_e2e_test_image_z9T8jK.png and b/packages/e2e-tests/assets/10x10_e2e_test_image_z9T8jK.png differ diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/code.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/code.test.js.snap index 50ee83d373c65e..3b30c08206ba0d 100644 --- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/code.test.js.snap +++ b/packages/e2e-tests/specs/editor/blocks/__snapshots__/code.test.js.snap @@ -5,3 +5,10 @@ exports[`Code can be created by three backticks and enter 1`] = ` <pre class=\\"wp-block-code\\"><code>&lt;?php</code></pre> <!-- /wp:code -->" `; + +exports[`Code should paste plain text 1`] = ` +"<!-- wp:code --> +<pre class=\\"wp-block-code\\"><code>&lt;img /> + &lt;br></code></pre> +<!-- /wp:code -->" +`; diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/image.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/image.test.js.snap index d17e4a8d39f6ac..18b856a57a4303 100644 --- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/image.test.js.snap +++ b/packages/e2e-tests/specs/editor/blocks/__snapshots__/image.test.js.snap @@ -1,5 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Image allows changing aspect ratio using the crop tools 1`] = `"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAACQf/xAAgEAAAAwkBAAAAAAAAAAAAAAAABAUBAwYJNTh0d7K1/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJTJUuninX570U4K8rVU7kPOmgoZKl08U6/PeinBXlaqnch500B//9k="`; + +exports[`Image allows changing aspect ratio using the crop tools 2`] = `"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAGAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAcJ/8QAIBAAAQIFBQAAAAAAAAAAAAAAAAQFAQMGCTU4dHeytf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCU2VNU9U8frvRbjV52yq3cTO0QAP/Z"`; + +exports[`Image allows rotating using the crop tools 1`] = `"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAACQf/xAAgEAAAAwkBAAAAAAAAAAAAAAAABAUBAwYJNTh0d7K1/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJTJUuninX570U4K8rVU7kPOmgoZKl08U6/PeinBXlaqnch500B//9k="`; + +exports[`Image allows rotating using the crop tools 2`] = `"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAcJ/8QAIRAAAAILAQAAAAAAAAAAAAAAAAUCBAcIFRlVVpOV0dT/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AlMlR6e/mVbUx8ISVHp7+ZVtTHwjV6LGtTW8yXQixrU1vMl0B/9k="`; + +exports[`Image allows zooming using the crop tools 1`] = `"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAACQf/xAAgEAAAAwkBAAAAAAAAAAAAAAAABAUBAwYJNTh0d7K1/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJTJUuninX570U4K8rVU7kPOmgoZKl08U6/PeinBXlaqnch500B//9k="`; + +exports[`Image allows zooming using the crop tools 2`] = `"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAFAAUDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAj/xAAaEAEAAQUAAAAAAAAAAAAAAAAABAcJOIW1/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AKptcYJ0y3XYmgA//9k="`; + exports[`Image should drag and drop files into media placeholder 1`] = ` "<!-- wp:image --> <figure class=\\"wp-block-image\\"><img alt=\\"\\"/></figure> diff --git a/packages/e2e-tests/specs/editor/blocks/code.test.js b/packages/e2e-tests/specs/editor/blocks/code.test.js index 039358aa0755c3..3747afec5e632d 100644 --- a/packages/e2e-tests/specs/editor/blocks/code.test.js +++ b/packages/e2e-tests/specs/editor/blocks/code.test.js @@ -6,6 +6,8 @@ import { clickBlockAppender, getEditedPostContent, createNewPost, + setClipboardData, + pressKeyWithModifier, } from '@wordpress/e2e-test-utils'; describe( 'Code', () => { @@ -32,4 +34,15 @@ describe( 'Code', () => { // Expect code block to be deleted. expect( await getEditedPostContent() ).toBe( '' ); } ); + + it( 'should paste plain text', async () => { + await insertBlock( 'Code' ); + + // Test to see if HTML and white space is kept. + await setClipboardData( { plainText: '<img />\n\t<br>' } ); + + await pressKeyWithModifier( 'primary', 'v' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/e2e-tests/specs/editor/blocks/image.test.js b/packages/e2e-tests/specs/editor/blocks/image.test.js index 5c4ce8b1cdf4fd..b254413a141df9 100644 --- a/packages/e2e-tests/specs/editor/blocks/image.test.js +++ b/packages/e2e-tests/specs/editor/blocks/image.test.js @@ -14,7 +14,10 @@ import { getEditedPostContent, createNewPost, clickButton, + clickBlockToolbarButton, + clickMenuItem, openDocumentSettingsSidebar, + pressKeyWithModifier, } from '@wordpress/e2e-test-utils'; async function upload( selector ) { @@ -41,6 +44,20 @@ async function waitForImage( filename ) { ); } +async function getSrc( elementHandle ) { + return elementHandle.evaluate( ( node ) => node.src ); +} +async function getDataURL( elementHandle ) { + return elementHandle.evaluate( ( node ) => { + const canvas = document.createElement( 'canvas' ); + const context = canvas.getContext( '2d' ); + canvas.width = node.width; + canvas.height = node.height; + context.drawImage( node, 0, 0 ); + return canvas.toDataURL( 'image/jpeg' ); + } ); +} + describe( 'Image', () => { beforeEach( async () => { await createNewPost(); @@ -161,4 +178,114 @@ describe( 'Image', () => { await waitForImage( fileName ); } ); + + it( 'allows zooming using the crop tools', async () => { + // Insert the block, upload a file and crop. + await insertBlock( 'Image' ); + const filename = await upload( '.wp-block-image input[type="file"]' ); + await waitForImage( filename ); + + // Assert that the image is initially unscaled and unedited. + const initialImage = await page.$( '.wp-block-image img' ); + const initialImageSrc = await getSrc( initialImage ); + const initialImageDataURL = await getDataURL( initialImage ); + expect( initialImageDataURL ).toMatchSnapshot(); + + // Zoom in to twice the amount using the zoom input. + await clickBlockToolbarButton( 'Crop' ); + await clickBlockToolbarButton( 'Zoom' ); + await page.waitForFunction( () => + document.activeElement.classList.contains( + 'components-range-control__slider' + ) + ); + await page.keyboard.press( 'Tab' ); + await page.waitForFunction( () => + document.activeElement.classList.contains( + 'components-input-control__input' + ) + ); + await pressKeyWithModifier( 'primary', 'a' ); + await page.keyboard.type( '200' ); + await page.keyboard.press( 'Escape' ); + await clickBlockToolbarButton( 'Apply', 'content' ); + + // Wait for the cropping tools to disappear. + await page.waitForSelector( + '.wp-block-image img:not( .reactEasyCrop_Image )' + ); + + // Assert that the image is edited. + const updatedImage = await page.$( '.wp-block-image img' ); + const updatedImageSrc = await getSrc( updatedImage ); + expect( initialImageSrc ).not.toEqual( updatedImageSrc ); + const updatedImageDataURL = await getDataURL( updatedImage ); + expect( initialImageDataURL ).not.toEqual( updatedImageDataURL ); + expect( updatedImageDataURL ).toMatchSnapshot(); + } ); + + it( 'allows changing aspect ratio using the crop tools', async () => { + // Insert the block, upload a file and crop. + await insertBlock( 'Image' ); + const filename = await upload( '.wp-block-image input[type="file"]' ); + await waitForImage( filename ); + + // Assert that the image is initially unscaled and unedited. + const initialImage = await page.$( '.wp-block-image img' ); + const initialImageSrc = await getSrc( initialImage ); + const initialImageDataURL = await getDataURL( initialImage ); + expect( initialImageDataURL ).toMatchSnapshot(); + + // Zoom in to twice the amount using the zoom input. + await clickBlockToolbarButton( 'Crop' ); + await clickBlockToolbarButton( 'Aspect Ratio' ); + await page.waitForFunction( () => + document.activeElement.classList.contains( + 'components-menu-item__button' + ) + ); + await clickMenuItem( '16:10' ); + await clickBlockToolbarButton( 'Apply', 'content' ); + + // Wait for the cropping tools to disappear. + await page.waitForSelector( + '.wp-block-image img:not( .reactEasyCrop_Image )' + ); + + // Assert that the image is edited. + const updatedImage = await page.$( '.wp-block-image img' ); + const updatedImageSrc = await getSrc( updatedImage ); + expect( initialImageSrc ).not.toEqual( updatedImageSrc ); + const updatedImageDataURL = await getDataURL( updatedImage ); + expect( initialImageDataURL ).not.toEqual( updatedImageDataURL ); + expect( updatedImageDataURL ).toMatchSnapshot(); + } ); + + it( 'allows rotating using the crop tools', async () => { + // Insert the block, upload a file and crop. + await insertBlock( 'Image' ); + const filename = await upload( '.wp-block-image input[type="file"]' ); + await waitForImage( filename ); + + // Assert that the image is initially unscaled and unedited. + const initialImage = await page.$( '.wp-block-image img' ); + const initialImageDataURL = await getDataURL( initialImage ); + expect( initialImageDataURL ).toMatchSnapshot(); + + // Double the image's size using the zoom input. + await clickBlockToolbarButton( 'Crop' ); + await page.waitForSelector( '.wp-block-image img.reactEasyCrop_Image' ); + await clickBlockToolbarButton( 'Rotate' ); + await clickBlockToolbarButton( 'Apply', 'content' ); + + await page.waitForSelector( + '.wp-block-image img:not( .reactEasyCrop_Image )' + ); + + // Assert that the image is edited. + const updatedImage = await page.$( '.wp-block-image img' ); + const updatedImageDataURL = await getDataURL( updatedImage ); + expect( initialImageDataURL ).not.toEqual( updatedImageDataURL ); + expect( updatedImageDataURL ).toMatchSnapshot(); + } ); } ); diff --git a/packages/e2e-tests/specs/editor/various/publish-button.test.js b/packages/e2e-tests/specs/editor/various/publish-button.test.js index 680ca0082c2eda..d9396b7f48c386 100644 --- a/packages/e2e-tests/specs/editor/various/publish-button.test.js +++ b/packages/e2e-tests/specs/editor/various/publish-button.test.js @@ -49,9 +49,10 @@ describe( 'PostPublishButton', () => { await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) ).toBeNull(); - await page.evaluate( () => - window.wp.data.dispatch( 'core/edit-post' ).requestMetaBoxUpdates() - ); + await page.evaluate( () => { + window.wp.data.dispatch( 'core/edit-post' ).requestMetaBoxUpdates(); + return true; + } ); expect( await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) ).not.toBeNull(); diff --git a/packages/e2e-tests/specs/experiments/blocks/post-title.test.js b/packages/e2e-tests/specs/experiments/blocks/post-title.test.js new file mode 100644 index 00000000000000..5e307541ff9cde --- /dev/null +++ b/packages/e2e-tests/specs/experiments/blocks/post-title.test.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { + activateTheme, + createNewPost, + insertBlock, + pressKeyWithModifier, + saveDraft, +} from '@wordpress/e2e-test-utils'; + +describe( 'Post Title block', () => { + beforeAll( async () => { + await activateTheme( 'twentytwentyone-blocks' ); + } ); + + afterAll( async () => { + await activateTheme( 'twentytwentyone' ); + } ); + + beforeEach( async () => { + await createNewPost(); + } ); + + it( 'Can edit the post title', async () => { + // Create a block with some text that will trigger a list creation. + await insertBlock( 'Post Title' ); + + // Select all of the text in the post title block. + await pressKeyWithModifier( 'primary', 'a' ); + + // Create a second list item. + await page.keyboard.type( 'Just tweaking the post title' ); + + await saveDraft(); + await page.reload(); + await page.waitForSelector( '.edit-post-layout' ); + const title = await page.$eval( + '.editor-post-title__input', + ( element ) => element.value + ); + expect( title ).toEqual( 'Just tweaking the post title' ); + } ); +} ); diff --git a/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js b/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js index d2d5034878ff61..e9108b50bbefc0 100644 --- a/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js +++ b/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js @@ -9,9 +9,10 @@ import { useEntityBlockEditor } from '@wordpress/core-data'; * Internal dependencies */ import { KIND, POST_TYPE } from '../../store/utils'; +import { store as editNavigationStore } from '../../store'; export default function useNavigationBlockEditor( post ) { - const { createMissingMenuItems } = useDispatch( 'core/edit-navigation' ); + const { createMissingMenuItems } = useDispatch( editNavigationStore ); const [ blocks, onInput, _onChange ] = useEntityBlockEditor( KIND, diff --git a/packages/edit-navigation/src/components/layout/use-navigation-editor.js b/packages/edit-navigation/src/components/layout/use-navigation-editor.js index 590f66ac0df9f7..bdc615828de3bf 100644 --- a/packages/edit-navigation/src/components/layout/use-navigation-editor.js +++ b/packages/edit-navigation/src/components/layout/use-navigation-editor.js @@ -4,6 +4,11 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { store as editNavigationStore } from '../../store'; + export default function useNavigationEditor() { const menus = useSelect( ( select ) => select( 'core' ).getMenus( { per_page: -1 } ), @@ -20,7 +25,7 @@ export default function useNavigationEditor() { const navigationPost = useSelect( ( select ) => - select( 'core/edit-navigation' ).getNavigationPostForMenu( + select( editNavigationStore ).getNavigationPostForMenu( selectedMenuId ), [ selectedMenuId ] diff --git a/packages/edit-navigation/src/components/toolbar/save-button.js b/packages/edit-navigation/src/components/toolbar/save-button.js index c5adfef8b8ac75..92132f47198d66 100644 --- a/packages/edit-navigation/src/components/toolbar/save-button.js +++ b/packages/edit-navigation/src/components/toolbar/save-button.js @@ -5,8 +5,13 @@ import { useDispatch } from '@wordpress/data'; import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { store as editNavigationStore } from '../../store'; + export default function SaveButton( { navigationPost } ) { - const { saveNavigationPost } = useDispatch( 'core/edit-navigation' ); + const { saveNavigationPost } = useDispatch( editNavigationStore ); return ( <Button diff --git a/packages/edit-navigation/src/store/constants.js b/packages/edit-navigation/src/store/constants.js new file mode 100644 index 00000000000000..8617d69ab054c5 --- /dev/null +++ b/packages/edit-navigation/src/store/constants.js @@ -0,0 +1,4 @@ +/** + * Module Constants + */ +export const STORE_NAME = 'core/edit-navigation'; diff --git a/packages/edit-navigation/src/store/controls.js b/packages/edit-navigation/src/store/controls.js index fb327b8fff8fcc..ef050130aafdec 100644 --- a/packages/edit-navigation/src/store/controls.js +++ b/packages/edit-navigation/src/store/controls.js @@ -8,6 +8,7 @@ import { createRegistryControl } from '@wordpress/data'; * Internal dependencies */ import { menuItemsQuery } from './utils'; +import { STORE_NAME } from './constants'; /** * Trigger an API Fetch request. @@ -72,7 +73,7 @@ export function getMenuItemToClientIdMapping( postId ) { export function getNavigationPostForMenu( menuId ) { return { type: 'SELECT', - registryName: 'core/edit-navigation', + registryName: STORE_NAME, selectorName: 'getNavigationPostForMenu', args: [ menuId ], }; @@ -173,7 +174,6 @@ const controls = { ), }; -const getState = ( registry ) => - registry.stores[ 'core/edit-navigation' ].store.getState(); +const getState = ( registry ) => registry.stores[ STORE_NAME ].store.getState(); export default controls; diff --git a/packages/edit-navigation/src/store/index.js b/packages/edit-navigation/src/store/index.js index 049f1785c9d205..b55979e5d11868 100644 --- a/packages/edit-navigation/src/store/index.js +++ b/packages/edit-navigation/src/store/index.js @@ -11,11 +11,7 @@ import * as resolvers from './resolvers'; import * as selectors from './selectors'; import * as actions from './actions'; import controls from './controls'; - -/** - * Module Constants - */ -const STORE_NAME = 'core/edit-navigation'; +import { STORE_NAME } from './constants'; /** * Block editor data store configuration. diff --git a/packages/edit-navigation/src/store/test/controls.js b/packages/edit-navigation/src/store/test/controls.js index 5e10991c294555..2b784d1e0322ce 100644 --- a/packages/edit-navigation/src/store/test/controls.js +++ b/packages/edit-navigation/src/store/test/controls.js @@ -17,6 +17,7 @@ import controls, { dispatch, } from '../controls'; import { menuItemsQuery } from '../utils'; +import { STORE_NAME } from '../constants'; // Mock it to prevent calling window.fetch in test environment jest.mock( '@wordpress/api-fetch', () => jest.fn( ( request ) => request ) ); @@ -63,7 +64,7 @@ describe( 'getNavigationPostForMenu', () => { it( 'has the correct type and payload', () => { expect( getNavigationPostForMenu( 123 ) ).toEqual( { type: 'SELECT', - registryName: 'core/edit-navigation', + registryName: STORE_NAME, selectorName: 'getNavigationPostForMenu', args: [ 123 ], } ); @@ -144,7 +145,7 @@ describe( 'controls', () => { }; const registry = { stores: { - 'core/edit-navigation': { + [ STORE_NAME ]: { store: { getState: jest.fn( () => state ), }, @@ -157,7 +158,7 @@ describe( 'controls', () => { ).toEqual( [ 'action1', 'action2' ] ); expect( - registry.stores[ 'core/edit-navigation' ].store.getState + registry.stores[ STORE_NAME ].store.getState ).toHaveBeenCalledTimes( 1 ); expect( @@ -167,7 +168,7 @@ describe( 'controls', () => { ).toEqual( [] ); expect( - registry.stores[ 'core/edit-navigation' ].store.getState + registry.stores[ STORE_NAME ].store.getState ).toHaveBeenCalledTimes( 2 ); } ); @@ -181,7 +182,7 @@ describe( 'controls', () => { }; const registry = { stores: { - 'core/edit-navigation': { + [ STORE_NAME ]: { store: { getState: jest.fn( () => state ), }, @@ -194,7 +195,7 @@ describe( 'controls', () => { ).toBe( true ); expect( - registry.stores[ 'core/edit-navigation' ].store.getState + registry.stores[ STORE_NAME ].store.getState ).toHaveBeenCalledTimes( 1 ); expect( @@ -204,7 +205,7 @@ describe( 'controls', () => { ).toBe( false ); expect( - registry.stores[ 'core/edit-navigation' ].store.getState + registry.stores[ STORE_NAME ].store.getState ).toHaveBeenCalledTimes( 2 ); } ); @@ -218,7 +219,7 @@ describe( 'controls', () => { }; const registry = { stores: { - 'core/edit-navigation': { + [ STORE_NAME ]: { store: { getState: jest.fn( () => state ), }, @@ -235,7 +236,7 @@ describe( 'controls', () => { } ); expect( - registry.stores[ 'core/edit-navigation' ].store.getState + registry.stores[ STORE_NAME ].store.getState ).toHaveBeenCalledTimes( 1 ); expect( @@ -245,7 +246,7 @@ describe( 'controls', () => { ).toEqual( {} ); expect( - registry.stores[ 'core/edit-navigation' ].store.getState + registry.stores[ STORE_NAME ].store.getState ).toHaveBeenCalledTimes( 2 ); } ); diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index bd8bc599f00d10..6447ac08981dfb 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -33,6 +33,7 @@ "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/data-controls": "file:../data-controls", "@wordpress/editor": "file:../editor", "@wordpress/element": "file:../element", "@wordpress/hooks": "file:../hooks", @@ -52,7 +53,6 @@ "classnames": "^2.2.5", "lodash": "^4.17.19", "memize": "^1.1.0", - "refx": "^3.0.0", "rememo": "^3.0.0" }, "publishConfig": { diff --git a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js index 853d2672431897..df18799c1cfa86 100644 --- a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js +++ b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js @@ -6,7 +6,7 @@ import TestRenderer, { act } from 'react-test-renderer'; /** * WordPress dependencies */ -import { RegistryProvider } from '@wordpress/data'; +import { RegistryProvider, createRegistry } from '@wordpress/data'; /** * Internal dependencies @@ -18,42 +18,62 @@ import { import { STORE_NAME } from '../../../store/constants'; describe( 'listener hook tests', () => { + const storeConfig = { + actions: { + forceUpdate: jest.fn( () => ( { type: 'FORCE_UPDATE' } ) ), + }, + reducer: ( state = {}, action ) => + action.type === 'FORCE_UPDATE' ? { ...state } : state, + }; const mockStores = { 'core/block-editor': { - getBlockSelectionStart: jest.fn(), + ...storeConfig, + selectors: { + getBlockSelectionStart: jest.fn(), + }, }, 'core/editor': { - getCurrentPost: jest.fn(), + ...storeConfig, + selectors: { + getCurrentPost: jest.fn(), + }, }, 'core/viewport': { - isViewportMatch: jest.fn(), + ...storeConfig, + selectors: { + isViewportMatch: jest.fn(), + }, }, [ STORE_NAME ]: { - isEditorSidebarOpened: jest.fn(), - openGeneralSidebar: jest.fn(), - closeGeneralSidebar: jest.fn(), - getActiveGeneralSidebarName: jest.fn(), - }, - }; - let subscribeTrigger; - const registry = { - select: jest - .fn() - .mockImplementation( ( storeName ) => mockStores[ storeName ] ), - dispatch: jest - .fn() - .mockImplementation( ( storeName ) => mockStores[ storeName ] ), - subscribe: ( subscription ) => { - subscribeTrigger = subscription; + ...storeConfig, + actions: { + ...storeConfig.actions, + openGeneralSidebar: jest.fn( () => ( { + type: 'OPEN_GENERAL_SIDEBAR', + } ) ), + closeGeneralSidebar: jest.fn( () => ( { + type: 'CLOSE_GENERAL_SIDEBAR', + } ) ), + }, + selectors: { + isEditorSidebarOpened: jest.fn(), + getActiveGeneralSidebarName: jest.fn(), + }, }, }; + + let registry; + beforeEach( () => { + registry = createRegistry( mockStores ); + } ); + const setMockReturnValue = ( store, functionName, value ) => { - mockStores[ store ][ functionName ] = jest - .fn() - .mockReturnValue( value ); + mockStores[ store ].selectors[ functionName ].mockReturnValue( value ); }; const getSpyedFunction = ( store, functionName ) => - mockStores[ store ][ functionName ]; + mockStores[ store ].selectors[ functionName ]; + const getSpyedAction = ( store, actionName ) => + mockStores[ store ].actions[ actionName ]; const renderComponent = ( testedHook, id, renderer = null ) => { const TestComponent = ( { postId } ) => { testedHook( postId ); @@ -70,11 +90,13 @@ describe( 'listener hook tests', () => { }; afterEach( () => { Object.values( mockStores ).forEach( ( storeMocks ) => { - Object.values( storeMocks ).forEach( ( mock ) => { + Object.values( storeMocks.selectors ).forEach( ( mock ) => { + mock.mockClear(); + } ); + Object.values( storeMocks.actions || {} ).forEach( ( mock ) => { mock.mockClear(); } ); } ); - subscribeTrigger = undefined; } ); describe( 'useBlockSelectionListener', () => { it( 'does nothing when editor sidebar is not open', () => { @@ -86,7 +108,7 @@ describe( 'listener hook tests', () => { getSpyedFunction( STORE_NAME, 'isEditorSidebarOpened' ) ).toHaveBeenCalled(); expect( - getSpyedFunction( STORE_NAME, 'openGeneralSidebar' ) + getSpyedAction( STORE_NAME, 'openGeneralSidebar' ) ).toHaveBeenCalledTimes( 0 ); } ); it( 'opens block sidebar if block is selected', () => { @@ -100,7 +122,7 @@ describe( 'listener hook tests', () => { renderComponent( useBlockSelectionListener, 10 ); } ); expect( - getSpyedFunction( STORE_NAME, 'openGeneralSidebar' ) + getSpyedAction( STORE_NAME, 'openGeneralSidebar' ) ).toHaveBeenCalledWith( 'edit-post/block' ); } ); it( 'opens document sidebar if block is not selected', () => { @@ -114,7 +136,7 @@ describe( 'listener hook tests', () => { renderComponent( useBlockSelectionListener, 10 ); } ); expect( - getSpyedFunction( STORE_NAME, 'openGeneralSidebar' ) + getSpyedAction( STORE_NAME, 'openGeneralSidebar' ) ).toHaveBeenCalledWith( 'edit-post/document' ); } ); } ); @@ -149,6 +171,9 @@ describe( 'listener hook tests', () => { expect( setAttribute ).not.toHaveBeenCalled(); } ); it( 'only calls document query selector once across renders', () => { + setMockReturnValue( 'core/editor', 'getCurrentPost', { + link: 'foo', + } ); act( () => { const renderer = renderComponent( useUpdatePostLinkListener, @@ -158,7 +183,7 @@ describe( 'listener hook tests', () => { } ); expect( mockSelector ).toHaveBeenCalledTimes( 1 ); act( () => { - subscribeTrigger(); + registry.dispatch( 'core/editor' ).forceUpdate(); } ); expect( mockSelector ).toHaveBeenCalledTimes( 1 ); } ); @@ -169,8 +194,9 @@ describe( 'listener hook tests', () => { act( () => { renderComponent( useUpdatePostLinkListener, 10 ); } ); + expect( setAttribute ).toHaveBeenCalledTimes( 1 ); act( () => { - subscribeTrigger(); + registry.dispatch( 'core/editor' ).forceUpdate(); } ); expect( setAttribute ).toHaveBeenCalledTimes( 1 ); } ); @@ -181,11 +207,14 @@ describe( 'listener hook tests', () => { act( () => { renderComponent( useUpdatePostLinkListener, 10 ); } ); + expect( setAttribute ).toHaveBeenCalledTimes( 1 ); + expect( setAttribute ).toHaveBeenCalledWith( 'href', 'foo' ); + setMockReturnValue( 'core/editor', 'getCurrentPost', { link: 'bar', } ); act( () => { - subscribeTrigger(); + registry.dispatch( 'core/editor' ).forceUpdate(); } ); expect( setAttribute ).toHaveBeenCalledTimes( 2 ); expect( setAttribute ).toHaveBeenCalledWith( 'href', 'bar' ); diff --git a/packages/edit-post/src/components/header/fullscreen-mode-close/index.js b/packages/edit-post/src/components/header/fullscreen-mode-close/index.js index d7da08c758470f..93d7f680487930 100644 --- a/packages/edit-post/src/components/header/fullscreen-mode-close/index.js +++ b/packages/edit-post/src/components/header/fullscreen-mode-close/index.js @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; import { wordpress } from '@wordpress/icons'; -function FullscreenModeClose( { showTooltip } ) { +function FullscreenModeClose( { showTooltip, icon, href } ) { const { isActive, isRequestingSiteIcon, postType, siteIconUrl } = useSelect( ( select ) => { const { getCurrentPostType } = select( 'core/editor' ); @@ -50,16 +50,26 @@ function FullscreenModeClose( { showTooltip } ) { src={ siteIconUrl } /> ); - } else if ( isRequestingSiteIcon ) { + } + + if ( isRequestingSiteIcon ) { buttonIcon = null; } + // Override default icon if custom icon is provided via props. + if ( icon ) { + buttonIcon = <Icon size="36px" icon={ icon } />; + } + return ( <Button className="edit-post-fullscreen-mode-close has-icon" - href={ addQueryArgs( 'edit.php', { - post_type: postType.slug, - } ) } + href={ + href ?? + addQueryArgs( 'edit.php', { + post_type: postType.slug, + } ) + } label={ get( postType, [ 'labels', 'view_items' ], __( 'Back' ) ) } showTooltip={ showTooltip } > diff --git a/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss b/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss index 825e2811d5c979..d24dd609328526 100644 --- a/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss +++ b/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss @@ -11,7 +11,7 @@ background: #23282e; // WP-admin gray. color: $white; border-radius: 0; - height: auto; + height: $header-height; width: $header-height; &:hover { @@ -31,4 +31,3 @@ .edit-post-fullscreen-mode-close_site-icon { width: 36px; } - diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index ba1b3e7d9366ed..cd5457ff3d29e3 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -8,10 +8,7 @@ import classnames from 'classnames'; */ import { PostSavedState, PostPreviewButton } from '@wordpress/editor'; import { useSelect } from '@wordpress/data'; -import { - PinnedItems, - __experimentalMainDashboardButton as MainDashboardButton, -} from '@wordpress/interface'; +import { PinnedItems } from '@wordpress/interface'; import { useViewportMatch } from '@wordpress/compose'; /** @@ -22,6 +19,7 @@ import HeaderToolbar from './header-toolbar'; import MoreMenu from './more-menu'; import PostPublishButtonOrToggle from './post-publish-button-or-toggle'; import { default as DevicePreview } from '../device-preview'; +import MainDashboardButton from '../header/main-dashboard-button'; function Header( { setEntitiesSavedStatesCallback } ) { const { diff --git a/packages/interface/src/components/main-dashboard-button/index.js b/packages/edit-post/src/components/header/main-dashboard-button/index.js similarity index 100% rename from packages/interface/src/components/main-dashboard-button/index.js rename to packages/edit-post/src/components/header/main-dashboard-button/index.js diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 85634df17ce5c1..9200c56812022c 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -17,6 +17,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { BlockBreadcrumb, __experimentalLibrary as Library, + __unstableUseEditorStyles as useEditorStyles, } from '@wordpress/block-editor'; import { Button, @@ -32,7 +33,7 @@ import { FullscreenMode, InterfaceSkeleton, } from '@wordpress/interface'; -import { useState, useEffect, useCallback } from '@wordpress/element'; +import { useState, useEffect, useCallback, useRef } from '@wordpress/element'; import { close } from '@wordpress/icons'; /** @@ -66,7 +67,7 @@ const interfaceLabels = { footer: __( 'Editor footer' ), }; -function Layout() { +function Layout( { settings } ) { const isMobileViewport = useViewportMatch( 'medium', '<' ); const isHugeViewport = useViewportMatch( 'huge', '>=' ); const { @@ -163,6 +164,9 @@ function Layout() { }, [ entitiesSavedStatesCallback ] ); + const ref = useRef(); + + useEditorStyles( ref, settings.styles ); return ( <> @@ -176,6 +180,7 @@ function Layout() { <SettingsSidebar /> <FocusReturnProvider> <InterfaceSkeleton + ref={ ref } className={ className } labels={ interfaceLabels } header={ diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index a177981e04f175..e51b8f4f691898 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -15,6 +15,7 @@ import { __unstableUseScrollMultiSelectionIntoView as useScrollMultiSelectionIntoView, __experimentalBlockSettingsMenuFirstItem, __experimentalUseResizeCanvas as useResizeCanvas, + __unstableUseCanvasClickRedirect as useCanvasClickRedirect, } from '@wordpress/block-editor'; import { Popover } from '@wordpress/components'; import { useRef } from '@wordpress/element'; @@ -30,13 +31,24 @@ export default function VisualEditor() { const deviceType = useSelect( ( select ) => { return select( 'core/edit-post' ).__experimentalGetPreviewDeviceType(); }, [] ); - const inlineStyles = useResizeCanvas( deviceType ); + const hasMetaBoxes = useSelect( + ( select ) => select( 'core/edit-post' ).hasMetaBoxes(), + [] + ); + const desktopCanvasStyles = { + height: '100%', + // Add a constant padding for the typewritter effect. When typing at the + // bottom, there needs to be room to scroll up. + paddingBottom: hasMetaBoxes ? null : '40vh', + }; + const resizedCanvasStyles = useResizeCanvas( deviceType ); useScrollMultiSelectionIntoView( ref ); useBlockSelectionClearer( ref ); useTypewriter( ref ); useClipboardHandler( ref ); useTypingObserver( ref ); + useCanvasClickRedirect( ref ); return ( <div className="edit-post-visual-editor"> @@ -46,7 +58,7 @@ export default function VisualEditor() { ref={ ref } className="editor-styles-wrapper" tabIndex="-1" - style={ inlineStyles } + style={ resizedCanvasStyles || desktopCanvasStyles } > <WritingFlow> <div className="edit-post-visual-editor__post-title-wrapper"> diff --git a/packages/edit-post/src/components/visual-editor/style.scss b/packages/edit-post/src/components/visual-editor/style.scss index 4534cdefe6886c..51099a6c339407 100644 --- a/packages/edit-post/src/components/visual-editor/style.scss +++ b/packages/edit-post/src/components/visual-editor/style.scss @@ -1,7 +1,5 @@ .edit-post-visual-editor { position: relative; - // Default background color so that grey .edit-post-editor-regions__content color doesn't show through. - background-color: $white; // The button element easily inherits styles that are meant for the editor style. // These rules enhance the specificity to reduce that inheritance. @@ -27,20 +25,16 @@ } } -.editor-styles-wrapper, -.editor-styles-wrapper > .block-editor-writing-flow__click-redirect { - height: 100%; -} +.editor-styles-wrapper { + // Default background color so that grey .edit-post-editor-regions__content + // color doesn't show through. + background-color: $white; -.edit-post-visual-editor .block-editor-writing-flow__click-redirect { - // Allow the page to be scrolled with the last block in the middle. - min-height: 40vh; - width: 100%; -} + cursor: text; -// Hide the extra space when there are metaboxes. -.has-metaboxes .edit-post-visual-editor .block-editor-writing-flow__click-redirect { - height: 0; + > * { + cursor: auto; + } } // Ideally this wrapper div is not needed but if we want to match the positioning of blocks diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index d7b01fb351f623..69fab90c77b5a6 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -146,7 +146,7 @@ class Editor extends Component { > <ErrorBoundary onError={ onError }> <EditorInitialization postId={ postId } /> - <Layout /> + <Layout settings={ settings } /> <KeyboardShortcuts shortcuts={ preventEventDiscovery } /> diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 0e0f3d8399a09b..fbf411d2c5d52f 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -162,3 +162,4 @@ export { default as PluginPrePublishPanel } from './components/sidebar/plugin-pr export { default as PluginSidebar } from './components/sidebar/plugin-sidebar'; export { default as PluginSidebarMoreMenuItem } from './components/header/plugin-sidebar-more-menu-item'; export { default as __experimentalFullscreenModeClose } from './components/header/fullscreen-mode-close'; +export { default as __experimentalMainDashboardButton } from './components/header/main-dashboard-button'; diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index dfb1373bb05c51..5dbbaed47b7096 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -1,12 +1,20 @@ /** * External dependencies */ -import { castArray } from 'lodash'; +import { castArray, reduce } from 'lodash'; /** * WordPress dependencies */ -import { controls } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { apiFetch } from '@wordpress/data-controls'; +import { controls, dispatch, select, subscribe } from '@wordpress/data'; +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import { getMetaBoxContainer } from '../utils/meta-boxes'; /** * Returns an action object used in signalling that the user opened an editor sidebar. @@ -153,11 +161,22 @@ export function toggleFeature( feature ) { }; } -export function switchEditorMode( mode ) { - return { +export function* switchEditorMode( mode ) { + yield { type: 'SWITCH_MODE', mode, }; + + // Unselect blocks when we switch to the code editor. + if ( mode !== 'visual' ) { + yield controls.dispatch( 'core/block-editor', 'clearSelectedBlock' ); + } + + const message = + mode === 'visual' + ? __( 'Visual editor selected' ) + : __( 'Code editor selected' ); + speak( message, 'assertive' ); } /** @@ -234,30 +253,136 @@ export function showBlockTypes( blockNames ) { }; } +let saveMetaboxUnsubscribe; + /** * Returns an action object used in signaling * what Meta boxes are available in which location. * * @param {Object} metaBoxesPerLocation Meta boxes per location. * - * @return {Object} Action object. + * @yield {Object} Action object. */ -export function setAvailableMetaBoxesPerLocation( metaBoxesPerLocation ) { - return { +export function* setAvailableMetaBoxesPerLocation( metaBoxesPerLocation ) { + yield { type: 'SET_META_BOXES_PER_LOCATIONS', metaBoxesPerLocation, }; + + const postType = yield controls.select( + 'core/editor', + 'getCurrentPostType' + ); + if ( window.postboxes.page !== postType ) { + window.postboxes.add_postbox_toggles( postType ); + } + + let wasSavingPost = yield controls.select( 'core/editor', 'isSavingPost' ); + let wasAutosavingPost = yield controls.select( + 'core/editor', + 'isAutosavingPost' + ); + + // Meta boxes are initialized once at page load. It is not necessary to + // account for updates on each state change. + // + // See: https://github.com/WordPress/WordPress/blob/5.1.1/wp-admin/includes/post.php#L2307-L2309 + const hasActiveMetaBoxes = yield controls.select( + 'core/edit-post', + 'hasMetaBoxes' + ); + + // First remove any existing subscription in order to prevent multiple saves + if ( !! saveMetaboxUnsubscribe ) { + saveMetaboxUnsubscribe(); + } + + // Save metaboxes when performing a full save on the post. + saveMetaboxUnsubscribe = subscribe( () => { + const isSavingPost = select( 'core/editor' ).isSavingPost(); + const isAutosavingPost = select( 'core/editor' ).isAutosavingPost(); + + // Save metaboxes on save completion, except for autosaves that are not a post preview. + const shouldTriggerMetaboxesSave = + hasActiveMetaBoxes && + wasSavingPost && + ! isSavingPost && + ! wasAutosavingPost; + + // Save current state for next inspection. + wasSavingPost = isSavingPost; + wasAutosavingPost = isAutosavingPost; + + if ( shouldTriggerMetaboxesSave ) { + dispatch( 'core/edit-post' ).requestMetaBoxUpdates(); + } + } ); } /** * Returns an action object used to request meta box update. * - * @return {Object} Action object. + * @yield {Object} Action object. */ -export function requestMetaBoxUpdates() { - return { +export function* requestMetaBoxUpdates() { + yield { type: 'REQUEST_META_BOX_UPDATES', }; + + // Saves the wp_editor fields + if ( window.tinyMCE ) { + window.tinyMCE.triggerSave(); + } + + // Additional data needed for backward compatibility. + // If we do not provide this data, the post will be overridden with the default values. + const post = yield controls.select( 'core/editor', 'getCurrentPost' ); + const additionalData = [ + post.comment_status ? [ 'comment_status', post.comment_status ] : false, + post.ping_status ? [ 'ping_status', post.ping_status ] : false, + post.sticky ? [ 'sticky', post.sticky ] : false, + post.author ? [ 'post_author', post.author ] : false, + ].filter( Boolean ); + + // We gather all the metaboxes locations data and the base form data + const baseFormData = new window.FormData( + document.querySelector( '.metabox-base-form' ) + ); + const activeMetaBoxLocations = yield controls.select( + 'core/edit-post', + 'getActiveMetaBoxLocations' + ); + const formDataToMerge = [ + baseFormData, + ...activeMetaBoxLocations.map( + ( location ) => + new window.FormData( getMetaBoxContainer( location ) ) + ), + ]; + + // Merge all form data objects into a single one. + const formData = reduce( + formDataToMerge, + ( memo, currentFormData ) => { + for ( const [ key, value ] of currentFormData ) { + memo.append( key, value ); + } + return memo; + }, + new window.FormData() + ); + additionalData.forEach( ( [ key, value ] ) => + formData.append( key, value ) + ); + + // Save the metaboxes + yield apiFetch( { + url: window._wpMetaBoxUrl, + method: 'POST', + body: formData, + parse: false, + } ); + yield controls.dispatch( 'core/edit-post', 'metaBoxUpdatesSuccess' ); } /** diff --git a/packages/edit-post/src/store/effects.js b/packages/edit-post/src/store/effects.js deleted file mode 100644 index 0e69bd005dd6f9..00000000000000 --- a/packages/edit-post/src/store/effects.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * External dependencies - */ -import { reduce } from 'lodash'; - -/** - * WordPress dependencies - */ -import { select, subscribe, dispatch } from '@wordpress/data'; -import { speak } from '@wordpress/a11y'; -import { __ } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; - -/** - * Internal dependencies - */ -import { metaBoxUpdatesSuccess, requestMetaBoxUpdates } from './actions'; -import { getActiveMetaBoxLocations } from './selectors'; -import { getMetaBoxContainer } from '../utils/meta-boxes'; - -let saveMetaboxUnsubscribe; - -const effects = { - SET_META_BOXES_PER_LOCATIONS( action, store ) { - // Allow toggling metaboxes panels - // We need to wait for all scripts to load - // If the meta box loads the post script, it will already trigger this. - // After merge in Core, make sure to drop the timeout and update the postboxes script - // to avoid the double binding. - setTimeout( () => { - const postType = select( 'core/editor' ).getCurrentPostType(); - if ( window.postboxes.page !== postType ) { - window.postboxes.add_postbox_toggles( postType ); - } - } ); - - let wasSavingPost = select( 'core/editor' ).isSavingPost(); - let wasAutosavingPost = select( 'core/editor' ).isAutosavingPost(); - - // Meta boxes are initialized once at page load. It is not necessary to - // account for updates on each state change. - // - // See: https://github.com/WordPress/WordPress/blob/5.1.1/wp-admin/includes/post.php#L2307-L2309 - const hasActiveMetaBoxes = select( 'core/edit-post' ).hasMetaBoxes(); - - // First remove any existing subscription in order to prevent multiple saves - if ( !! saveMetaboxUnsubscribe ) { - saveMetaboxUnsubscribe(); - } - - // Save metaboxes when performing a full save on the post. - saveMetaboxUnsubscribe = subscribe( () => { - const isSavingPost = select( 'core/editor' ).isSavingPost(); - const isAutosavingPost = select( 'core/editor' ).isAutosavingPost(); - - // Save metaboxes on save completion, except for autosaves that are not a post preview. - const shouldTriggerMetaboxesSave = - hasActiveMetaBoxes && - wasSavingPost && - ! isSavingPost && - ! wasAutosavingPost; - - // Save current state for next inspection. - wasSavingPost = isSavingPost; - wasAutosavingPost = isAutosavingPost; - - if ( shouldTriggerMetaboxesSave ) { - store.dispatch( requestMetaBoxUpdates() ); - } - } ); - }, - REQUEST_META_BOX_UPDATES( action, store ) { - // Saves the wp_editor fields - if ( window.tinyMCE ) { - window.tinyMCE.triggerSave(); - } - - const state = store.getState(); - - // Additional data needed for backward compatibility. - // If we do not provide this data, the post will be overridden with the default values. - const post = select( 'core/editor' ).getCurrentPost( state ); - const additionalData = [ - post.comment_status - ? [ 'comment_status', post.comment_status ] - : false, - post.ping_status ? [ 'ping_status', post.ping_status ] : false, - post.sticky ? [ 'sticky', post.sticky ] : false, - post.author ? [ 'post_author', post.author ] : false, - ].filter( Boolean ); - - // We gather all the metaboxes locations data and the base form data - const baseFormData = new window.FormData( - document.querySelector( '.metabox-base-form' ) - ); - const formDataToMerge = [ - baseFormData, - ...getActiveMetaBoxLocations( state ).map( - ( location ) => - new window.FormData( getMetaBoxContainer( location ) ) - ), - ]; - - // Merge all form data objects into a single one. - const formData = reduce( - formDataToMerge, - ( memo, currentFormData ) => { - for ( const [ key, value ] of currentFormData ) { - memo.append( key, value ); - } - return memo; - }, - new window.FormData() - ); - additionalData.forEach( ( [ key, value ] ) => - formData.append( key, value ) - ); - - // Save the metaboxes - apiFetch( { - url: window._wpMetaBoxUrl, - method: 'POST', - body: formData, - parse: false, - } ).then( () => store.dispatch( metaBoxUpdatesSuccess() ) ); - }, - SWITCH_MODE( action ) { - // Unselect blocks when we switch to the code editor. - if ( action.mode !== 'visual' ) { - dispatch( 'core/block-editor' ).clearSelectedBlock(); - } - - const message = - action.mode === 'visual' - ? __( 'Visual editor selected' ) - : __( 'Code editor selected' ); - speak( message, 'assertive' ); - }, -}; - -export default effects; diff --git a/packages/edit-post/src/store/index.js b/packages/edit-post/src/store/index.js index 7d08516f251a42..c461e54c601fc6 100644 --- a/packages/edit-post/src/store/index.js +++ b/packages/edit-post/src/store/index.js @@ -2,12 +2,12 @@ * WordPress dependencies */ import { createReduxStore, registerStore } from '@wordpress/data'; +import { controls } from '@wordpress/data-controls'; /** * Internal dependencies */ import reducer from './reducer'; -import applyMiddlewares from './middlewares'; import * as actions from './actions'; import * as selectors from './selectors'; import { STORE_NAME } from './constants'; @@ -16,6 +16,7 @@ const storeConfig = { reducer, actions, selectors, + controls, persist: [ 'preferences' ], }; @@ -29,6 +30,4 @@ const storeConfig = { export const store = createReduxStore( STORE_NAME, storeConfig ); // Ideally we use register instead of register store. -// We shouuld be able to make the switch once we remove the effects. -const instantiatedStore = registerStore( STORE_NAME, storeConfig ); -applyMiddlewares( instantiatedStore ); +registerStore( STORE_NAME, storeConfig ); diff --git a/packages/edit-post/src/store/middlewares.js b/packages/edit-post/src/store/middlewares.js deleted file mode 100644 index 06cacf026d58c2..00000000000000 --- a/packages/edit-post/src/store/middlewares.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * External dependencies - */ -import { flowRight } from 'lodash'; -import refx from 'refx'; - -/** - * Internal dependencies - */ -import effects from './effects'; - -/** - * Applies the custom middlewares used specifically in the editor module. - * - * @param {Object} store Store Object. - * - * @return {Object} Update Store Object. - */ -function applyMiddlewares( store ) { - const middlewares = [ refx( effects ) ]; - - let enhancedDispatch = () => { - throw new Error( - 'Dispatching while constructing your middleware is not allowed. ' + - 'Other middleware would not be applied to this dispatch.' - ); - }; - let chain = []; - - const middlewareAPI = { - getState: store.getState, - dispatch: ( ...args ) => enhancedDispatch( ...args ), - }; - chain = middlewares.map( ( middleware ) => middleware( middlewareAPI ) ); - enhancedDispatch = flowRight( ...chain )( store.dispatch ); - - store.dispatch = enhancedDispatch; - return store; -} - -export default applyMiddlewares; diff --git a/packages/edit-post/src/store/test/actions.js b/packages/edit-post/src/store/test/actions.js index 31877f378bb286..fc398c3aeb2117 100644 --- a/packages/edit-post/src/store/test/actions.js +++ b/packages/edit-post/src/store/test/actions.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { controls } from '@wordpress/data'; + /** * Internal dependencies */ @@ -95,9 +100,17 @@ describe( 'actions', () => { } ); describe( 'requestMetaBoxUpdates', () => { - it( 'should return the REQUEST_META_BOX_UPDATES action', () => { - expect( requestMetaBoxUpdates() ).toEqual( { - type: 'REQUEST_META_BOX_UPDATES', + it( 'should yield the REQUEST_META_BOX_UPDATES action', () => { + const fulfillment = requestMetaBoxUpdates(); + expect( fulfillment.next() ).toEqual( { + done: false, + value: { + type: 'REQUEST_META_BOX_UPDATES', + }, + } ); + expect( fulfillment.next() ).toEqual( { + done: false, + value: controls.select( 'core/editor', 'getCurrentPost' ), } ); } ); } ); diff --git a/packages/edit-site/src/components/editor/global-styles-provider.js b/packages/edit-site/src/components/editor/global-styles-provider.js index 544bd7f20eb2e5..fcc8341293bd25 100644 --- a/packages/edit-site/src/components/editor/global-styles-provider.js +++ b/packages/edit-site/src/components/editor/global-styles-provider.js @@ -20,12 +20,12 @@ import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { default as getGlobalStyles } from './global-styles-renderer'; import { GLOBAL_CONTEXT, getValueFromVariable, getPresetVariable, } from './utils'; +import getGlobalStyles from './global-styles-renderer'; const EMPTY_CONTENT = '{}'; @@ -200,7 +200,18 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { css: getGlobalStyles( contexts, mergedStyles, - STYLE_PROPERTY + STYLE_PROPERTY, + 'cssVariables' + ), + isGlobalStyles: true, + __experimentalNoWrapper: true, + }, + { + css: getGlobalStyles( + contexts, + mergedStyles, + STYLE_PROPERTY, + 'blockStyles' ), isGlobalStyles: true, }, diff --git a/packages/edit-site/src/components/editor/global-styles-renderer.js b/packages/edit-site/src/components/editor/global-styles-renderer.js index e0dc01f4c9777a..12b606a6a6362f 100644 --- a/packages/edit-site/src/components/editor/global-styles-renderer.js +++ b/packages/edit-site/src/components/editor/global-styles-renderer.js @@ -26,141 +26,159 @@ function compileStyleValue( uncompiledValue ) { return uncompiledValue; } -export default ( blockData, tree, metadata ) => { - const styles = []; - // Can this be converted to a context, as the global context? - // See comment in the server. - styles.push( LINK_COLOR_DECLARATION ); +/** + * Transform given preset tree into a set of style declarations. + * + * @param {Object} blockPresets + * + * @return {Array} An array of style declarations. + */ +function getBlockPresetsDeclarations( blockPresets = {} ) { + return reduce( + PRESET_CATEGORIES, + ( declarations, { path, key }, category ) => { + const preset = get( blockPresets, path, [] ); + preset.forEach( ( value ) => { + declarations.push( + `--wp--preset--${ kebabCase( category ) }--${ + value.slug + }: ${ value[ key ] }` + ); + } ); + return declarations; + }, + [] + ); +} + +/** + * Transform given preset tree into a set of preset class declarations. + * + * @param {string} blockSelector + * @param {Object} blockPresets + * @return {string} CSS declarations for the preset classes. + */ +function getBlockPresetClasses( blockSelector, blockPresets = {} ) { + return reduce( + PRESET_CLASSES, + ( declarations, { path, key, property }, classSuffix ) => { + const presets = get( blockPresets, path, [] ); + presets.forEach( ( preset ) => { + const slug = preset.slug; + const value = preset[ key ]; + const classSelectorToUse = `.has-${ slug }-${ classSuffix }`; + const selectorToUse = `${ blockSelector }${ classSelectorToUse }`; + declarations += `${ selectorToUse } {${ property }: ${ value };}`; + } ); + return declarations; + }, + '' + ); +} - /** - * Transform given style tree into a set of style declarations. - * - * @param {Object} blockSupports What styles the block supports. - * @param {Object} blockStyles Block styles. - * - * @return {Array} An array of style declarations. - */ - const getBlockStylesDeclarations = ( blockSupports, blockStyles = {} ) => { - const declarations = []; - Object.keys( metadata ).forEach( ( key ) => { +function flattenTree( input = {}, prefix, token ) { + let result = []; + Object.keys( input ).forEach( ( key ) => { + const newKey = prefix + kebabCase( key.replace( '/', '-' ) ); + const newLeaf = input[ key ]; + + if ( newLeaf instanceof Object ) { + const newPrefix = newKey + token; + result = [ ...result, ...flattenTree( newLeaf, newPrefix, token ) ]; + } else { + result.push( `${ newKey }: ${ newLeaf }` ); + } + } ); + return result; +} + +/** + * Transform given style tree into a set of style declarations. + * + * @param {Object} blockSupports What styles the block supports. + * @param {Object} blockStyles Block styles. + * @param {Object} metadata Block styles metadata information. + * + * @return {Array} An array of style declarations. + */ +function getBlockStylesDeclarations( + blockSupports, + blockStyles = {}, + metadata +) { + return reduce( + metadata, + ( declarations, { value }, key ) => { const cssProperty = key.startsWith( '--' ) ? key : kebabCase( key ); if ( blockSupports.includes( key ) && - get( blockStyles, metadata[ key ].value, false ) + get( blockStyles, value, false ) ) { declarations.push( `${ cssProperty }: ${ compileStyleValue( - get( blockStyles, metadata[ key ].value ) + get( blockStyles, value ) ) }` ); } - } ); - - return declarations; - }; + return declarations; + }, + [] + ); +} - /** - * Transform given preset tree into a set of preset class declarations. - * - * @param {string} blockSelector - * @param {Object} blockPresets - * @return {string} CSS declarations for the preset classes. - */ - const getBlockPresetClasses = ( blockSelector, blockPresets = {} ) => { - return reduce( - PRESET_CLASSES, - ( declarations, { path, key, property }, classSuffix ) => { - const presets = get( blockPresets, path, [] ); - presets.forEach( ( preset ) => { - const slug = preset.slug; - const value = preset[ key ]; - const classSelectorToUse = `.has-${ slug }-${ classSuffix }`; - const selectorToUse = `${ blockSelector }${ classSelectorToUse }`; - declarations += `${ selectorToUse } {${ property }: ${ value };}`; - } ); - return declarations; - }, - '' - ); - }; +export default ( blockData, tree, metadata, type = 'all' ) => { + return reduce( + blockData, + ( styles, { selector }, context ) => { + if ( type === 'all' || type === 'cssVariables' ) { + const variableDeclarations = [ + ...getBlockPresetsDeclarations( + tree?.[ context ]?.settings + ), + ...flattenTree( + tree?.[ context ]?.settings?.custom, + '--wp--custom--', + '--' + ), + ]; - /** - * Transform given preset tree into a set of style declarations. - * - * @param {Object} blockPresets - * - * @return {Array} An array of style declarations. - */ - const getBlockPresetsDeclarations = ( blockPresets = {} ) => { - return reduce( - PRESET_CATEGORIES, - ( declarations, { path, key }, category ) => { - const preset = get( blockPresets, path, [] ); - preset.forEach( ( value ) => { - declarations.push( - `--wp--preset--${ kebabCase( category ) }--${ - value.slug - }: ${ value[ key ] }` + if ( variableDeclarations.length > 0 ) { + styles.push( + `${ selector } { ${ variableDeclarations.join( + ';' + ) } }` ); - } ); - return declarations; - }, - [] - ); - }; - - const flattenTree = ( input, prefix, token ) => { - let result = []; - Object.keys( input ).forEach( ( key ) => { - const newKey = prefix + kebabCase( key.replace( '/', '-' ) ); - const newLeaf = input[ key ]; - - if ( newLeaf instanceof Object ) { - const newPrefix = newKey + token; - result = [ - ...result, - ...flattenTree( newLeaf, newPrefix, token ), - ]; - } else { - result.push( `${ newKey }: ${ newLeaf }` ); + } } - } ); - return result; - }; - - const getCustomDeclarations = ( blockCustom = {} ) => { - if ( Object.keys( blockCustom ).length === 0 ) { - return []; - } - - return flattenTree( blockCustom, '--wp--custom--', '--' ); - }; - - Object.keys( blockData ).forEach( ( context ) => { - const blockSelector = blockData[ context ].selector; - - const blockDeclarations = [ - ...getBlockStylesDeclarations( - blockData[ context ].supports, - tree?.[ context ]?.styles - ), - ...getBlockPresetsDeclarations( tree?.[ context ]?.settings ), - ...getCustomDeclarations( tree?.[ context ]?.settings?.custom ), - ]; - if ( blockDeclarations.length > 0 ) { - styles.push( - `${ blockSelector } { ${ blockDeclarations.join( ';' ) } }` - ); - } + if ( type === 'all' || type === 'blockStyles' ) { + const blockStyleDeclarations = getBlockStylesDeclarations( + blockData[ context ].supports, + tree?.[ context ]?.styles, + metadata + ); - const presetClasses = getBlockPresetClasses( - blockSelector, - tree?.[ context ]?.settings - ); - if ( presetClasses ) { - styles.push( presetClasses ); - } - } ); + if ( blockStyleDeclarations.length > 0 ) { + styles.push( + `${ selector } { ${ blockStyleDeclarations.join( + ';' + ) } }` + ); + } - return styles.join( '' ); + const presetClasses = getBlockPresetClasses( + selector, + tree?.[ context ]?.settings + ); + if ( presetClasses ) { + styles.push( presetClasses ); + } + } + return styles; + }, + // Can this be converted to a context, as the global context? + // See comment in the server. + type === 'all' || type === 'blockStyles' + ? [ LINK_COLOR_DECLARATION ] + : [] + ).join( '' ); }; diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 93067f58455a2d..db7cf89b71ad36 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -1,7 +1,13 @@ /** * WordPress dependencies */ -import { useEffect, useState, useMemo, useCallback } from '@wordpress/element'; +import { + useEffect, + useState, + useMemo, + useCallback, + useRef, +} from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { SlotFillProvider, @@ -15,7 +21,7 @@ import { BlockContextProvider, BlockSelectionClearer, BlockBreadcrumb, - __unstableEditorStyles as EditorStyles, + __unstableUseEditorStyles as useEditorStyles, __experimentalUseResizeCanvas as useResizeCanvas, __experimentalLibrary as Library, } from '@wordpress/block-editor'; @@ -134,22 +140,15 @@ function Editor() { const { __experimentalGetTemplateInfo: getTemplateInfo, } = select( 'core/editor' ); - entitiesToSave.forEach( ( { kind, name, key } ) => { + entitiesToSave.forEach( ( { kind, name, key, title } ) => { const record = getEditedEntityRecord( kind, name, key ); - - if ( 'postType' === kind && name === 'wp_template' ) { - const { title } = getTemplateInfo( record ); - return editEntityRecord( kind, name, key, { - status: 'publish', - title, - } ); + if ( kind === 'postType' && name === 'wp_template' ) { + ( { title } = getTemplateInfo( record ) ); } - - const edits = record.slug - ? { status: 'publish', title: record.slug } - : { status: 'publish' }; - - editEntityRecord( kind, name, key, edits ); + editEntityRecord( kind, name, key, { + status: 'publish', + title: title || record.slug, + } ); } ); } setIsEntitiesSavedStatesOpen( false ); @@ -191,10 +190,12 @@ function Editor() { }, [ isNavigationOpen ] ); const isMobile = useViewportMatch( 'medium', '<' ); + const ref = useRef(); + + useEditorStyles( ref, settings.styles ); return ( <> - <EditorStyles styles={ settings.styles } /> <FullscreenMode isActive={ isFullscreenActive } /> <UnsavedChangesWarning /> <SlotFillProvider> @@ -235,6 +236,7 @@ function Editor() { <KeyboardShortcuts.Register /> <SidebarComplementaryAreaFills /> <InterfaceSkeleton + ref={ ref } labels={ interfaceLabels } drawer={ <NavigationSidebar /> diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss index 66c0a582f5760f..cb8048a51cd5c4 100644 --- a/packages/edit-site/src/components/editor/style.scss +++ b/packages/edit-site/src/components/editor/style.scss @@ -21,6 +21,7 @@ } .edit-site-visual-editor { + position: relative; background-color: $white; } diff --git a/packages/edit-site/src/components/header/style.scss b/packages/edit-site/src/components/header/style.scss index 8d08537805bb50..2431fe656f286d 100644 --- a/packages/edit-site/src/components/header/style.scss +++ b/packages/edit-site/src/components/header/style.scss @@ -5,39 +5,27 @@ height: $header-height; box-sizing: border-box; width: 100%; + justify-content: space-between; @include break-medium() { - padding-left: 60px; - transition: padding-left 20ms linear; - transition-delay: 80ms; - @include reduce-motion("transition"); + body.is-fullscreen-mode & { + padding-left: 60px; + transition: padding-left 20ms linear; + transition-delay: 80ms; + @include reduce-motion("transition"); + } + } .edit-site-header_start, .edit-site-header_end { - flex: 1 0; display: flex; } - @include break-medium() { - .edit-site-header_start { - // Flex basis prevents the header_start toolbar - // from collapsing when shrinking the viewport. - flex-basis: calc(#{$header-toolbar-min-width} - #{$header-height}); - } - - .edit-site-header_end { - // Flex basis prevents the header_end toolbar - // from collapsing when shrinking the viewport - flex-basis: $header-toolbar-min-width; - } - } - .edit-site-header_center { display: flex; align-items: center; height: 100%; - flex-shrink: 1; // Flex items will, by default, refuse to shrink below a minimum // intrinsic width. In order to shrink this flexbox item, and // subsequently truncate child text, we set an explicit min-width. @@ -56,9 +44,18 @@ body.is-navigation-sidebar-open { padding-left: 0; transition: padding-left 20ms linear; transition-delay: 0ms; + } +} - .edit-site-header_start { - flex-basis: $header-toolbar-min-width; +// Centred document title on small screens with sidebar open +@media ( max-width: #{ ($break-large - 1) } ) { + body.is-navigation-sidebar-open .edit-site-header { + .edit-site-header-toolbar__inserter-toggle ~ .components-button, + .edit-site-header_end .components-button:not(.is-primary) { + display: none; + } + .edit-site-save-button__button { + margin-right: 0; } } } diff --git a/packages/edit-site/src/components/main-dashboard-button/index.js b/packages/edit-site/src/components/main-dashboard-button/index.js new file mode 100644 index 00000000000000..efc3bfaad19e87 --- /dev/null +++ b/packages/edit-site/src/components/main-dashboard-button/index.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { + __experimentalUseSlot as useSlot, + createSlotFill, +} from '@wordpress/components'; + +const slotName = '__experimentalMainDashboardButton'; + +const { Fill, Slot: MainDashboardButtonSlot } = createSlotFill( slotName ); + +const MainDashboardButton = Fill; + +const Slot = ( { children } ) => { + const slot = useSlot( slotName ); + const hasFills = Boolean( slot.fills && slot.fills.length ); + + if ( ! hasFills ) { + return children; + } + + return <MainDashboardButtonSlot bubblesVirtually />; +}; + +MainDashboardButton.Slot = Slot; + +export default MainDashboardButton; diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js index 5738942e4779d9..e0201af3420970 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js @@ -11,6 +11,16 @@ export const TEMPLATES_POSTS = [ 'home', 'single' ]; export const TEMPLATES_STATUSES = [ 'publish', 'draft', 'auto-draft' ]; +export const TEMPLATES_NEW_OPTIONS = [ + 'front-page', + 'single-post', + 'page', + 'archive', + 'search', + '404', + 'index', +]; + export const MENU_ROOT = 'root'; export const MENU_CONTENT_CATEGORIES = 'content-categories'; export const MENU_CONTENT_PAGES = 'content-pages'; diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/template-parts.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/template-parts.js index ad597671aba41f..c2d8a919714d9a 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/template-parts.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/template-parts.js @@ -9,17 +9,24 @@ import { map } from 'lodash'; import { __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; import { - __experimentalNavigationItem as NavigationItem, __experimentalNavigationMenu as NavigationMenu, + __experimentalNavigationItem as NavigationItem, } from '@wordpress/components'; +import { useState, useCallback } from '@wordpress/element'; /** * Internal dependencies */ import TemplateNavigationItem from '../template-navigation-item'; import { MENU_ROOT, MENU_TEMPLATE_PARTS } from '../constants'; +import SearchResults from '../search-results'; export default function TemplatePartsMenu() { + const [ search, setSearch ] = useState( '' ); + const onSearch = useCallback( ( value ) => { + setSearch( value ); + } ); + const templateParts = useSelect( ( select ) => { const unfilteredTemplateParts = select( 'core' ).getEntityRecords( 'postType', 'wp_template_part', { @@ -38,15 +45,25 @@ export default function TemplatePartsMenu() { menu={ MENU_TEMPLATE_PARTS } title={ __( 'Template Parts' ) } parentMenu={ MENU_ROOT } + hasSearch={ true } + onSearch={ onSearch } + search={ search } > - { map( templateParts, ( templatePart ) => ( - <TemplateNavigationItem - item={ templatePart } - key={ `wp_template_part-${ templatePart.id }` } - /> - ) ) } + { search && ( + <SearchResults items={ templateParts } search={ search } /> + ) } + + { ! search && + map( templateParts, ( templatePart ) => ( + <TemplateNavigationItem + item={ templatePart } + key={ `wp_template_part-${ templatePart.id }` } + /> + ) ) } - { ! templateParts && <NavigationItem title={ __( 'Loading…' ) } /> } + { ! search && templateParts === null && ( + <NavigationItem title={ __( 'Loading…' ) } isText /> + ) } </NavigationMenu> ); } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js index e96aaef7f8d990..c3f15d44d10ed2 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js @@ -12,6 +12,7 @@ import { } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { __, _x } from '@wordpress/i18n'; +import { useState, useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -30,8 +31,14 @@ import { import TemplatesAllMenu from './templates-all'; import NewTemplateDropdown from '../new-template-dropdown'; import TemplateNavigationItem from '../template-navigation-item'; +import SearchResults from '../search-results'; export default function TemplatesMenu() { + const [ search, setSearch ] = useState( '' ); + const onSearch = useCallback( ( value ) => { + setSearch( value ); + } ); + const templates = useSelect( ( select ) => select( 'core' ).getEntityRecords( 'postType', 'wp_template', { @@ -51,27 +58,43 @@ export default function TemplatesMenu() { title={ __( 'Templates' ) } titleAction={ <NewTemplateDropdown /> } parentMenu={ MENU_ROOT } + hasSearch={ true } + onSearch={ onSearch } + search={ search } > - { map( generalTemplates, ( template ) => ( - <TemplateNavigationItem - item={ template } - key={ `wp_template-${ template.id }` } - /> - ) ) } - <NavigationItem - navigateToMenu={ MENU_TEMPLATES_ALL } - title={ _x( 'All', 'all templates' ) } - /> - <NavigationItem - navigateToMenu={ MENU_TEMPLATES_PAGES } - title={ __( 'Pages' ) } - hideIfTargetMenuEmpty - /> - <NavigationItem - navigateToMenu={ MENU_TEMPLATES_POSTS } - title={ __( 'Posts' ) } - hideIfTargetMenuEmpty - /> + { search && ( + <SearchResults items={ templates } search={ search } /> + ) } + + { ! search && ( + <> + <NavigationItem + navigateToMenu={ MENU_TEMPLATES_ALL } + title={ _x( 'All', 'all templates' ) } + /> + <NavigationItem + navigateToMenu={ MENU_TEMPLATES_PAGES } + title={ __( 'Pages' ) } + hideIfTargetMenuEmpty + /> + <NavigationItem + navigateToMenu={ MENU_TEMPLATES_POSTS } + title={ __( 'Posts' ) } + hideIfTargetMenuEmpty + /> + { map( generalTemplates, ( template ) => ( + <TemplateNavigationItem + item={ template } + key={ `wp_template-${ template.id }` } + /> + ) ) } + </> + ) } + + { ! search && templates === null && ( + <NavigationItem title={ __( 'Loading…' ) } isText /> + ) } + <TemplatesPostsMenu templates={ templates } /> <TemplatesPagesMenu templates={ templates } /> <TemplatesAllMenu templates={ templates } /> diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/new-template-dropdown.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/new-template-dropdown.js index 88903d2d2d40cb..c428472a051f2d 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/new-template-dropdown.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/new-template-dropdown.js @@ -20,7 +20,7 @@ import { Icon, plus } from '@wordpress/icons'; * Internal dependencies */ import getClosestAvailableTemplate from '../../../utils/get-closest-available-template'; -import { TEMPLATES_STATUSES } from './constants'; +import { TEMPLATES_NEW_OPTIONS, TEMPLATES_STATUSES } from './constants'; export default function NewTemplateDropdown() { const { defaultTemplateTypes, templates } = useSelect( ( select ) => { @@ -59,9 +59,15 @@ export default function NewTemplateDropdown() { const missingTemplates = filter( defaultTemplateTypes, - ( template ) => ! includes( existingTemplateSlugs, template.slug ) + ( template ) => + includes( TEMPLATES_NEW_OPTIONS, template.slug ) && + ! includes( existingTemplateSlugs, template.slug ) ); + if ( ! missingTemplates.length ) { + return null; + } + return ( <DropdownMenu className="edit-site-navigation-panel__new-template-dropdown" @@ -77,7 +83,7 @@ export default function NewTemplateDropdown() { } } > { ( { onClose } ) => ( - <NavigableMenu> + <NavigableMenu className="edit-site-navigation-panel__new-template-popover"> <MenuGroup label={ __( 'Add Template' ) }> { map( missingTemplates, diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/search-results.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/search-results.js new file mode 100644 index 00000000000000..39e7b60cb1781c --- /dev/null +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/search-results.js @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +import { map } from 'lodash'; + +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; +import { __experimentalNavigationGroup as NavigationGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { normalizedSearch } from './utils'; +import { useSelect } from '@wordpress/data'; +import TemplateNavigationItem from './template-navigation-item'; + +export default function SearchResults( { items, search } ) { + const itemType = items?.length > 0 ? items[ 0 ].type : null; + + const itemInfos = useSelect( + ( select ) => { + if ( itemType === 'wp_template' ) { + const { + __experimentalGetTemplateInfo: getTemplateInfo, + } = select( 'core/editor' ); + + return items.map( ( item ) => ( { + slug: item.slug, + ...getTemplateInfo( item ), + } ) ); + } + + return items.map( ( item ) => ( { + slug: item.slug, + title: item.title?.rendered, + description: item.excerpt?.rendered, + } ) ); + }, + [ items, itemType ] + ); + + const itemsFiltered = useMemo( () => { + if ( items === null || search.length === 0 ) { + return []; + } + + return items.filter( ( { slug } ) => { + const { title, description } = itemInfos.find( + ( info ) => info.slug === slug + ); + + return ( + normalizedSearch( slug, search ) || + normalizedSearch( title, search ) || + normalizedSearch( description, search ) + ); + } ); + }, [ items, itemInfos, search ] ); + + return ( + <NavigationGroup title={ __( 'Search results' ) }> + { map( itemsFiltered, ( item ) => ( + <TemplateNavigationItem + item={ item } + key={ `${ item.type }-${ item.id }` } + /> + ) ) } + </NavigationGroup> + ); +} diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/style.scss b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/style.scss index c4b53d124fd426..d4fe4b544deca2 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/style.scss +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/style.scss @@ -114,10 +114,14 @@ } .edit-site-navigation-panel__new-template-dropdown { - display: flex; margin: 0 0 0 $grid-unit-15; button { margin: 0; } } +.edit-site-navigation-panel__new-template-popover { + @include break-small() { + min-width: 300px; + } +} diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/templates-navigation.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/templates-navigation.js index 44dd8765be8554..55bcbf88439ea0 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/templates-navigation.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/templates-navigation.js @@ -7,7 +7,6 @@ import { __experimentalNavigationItem as NavigationItem, __experimentalNavigationBackButton as NavigationBackButton, } from '@wordpress/components'; -import { __experimentalMainDashboardButton as MainDashboardButton } from '@wordpress/interface'; import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -16,6 +15,7 @@ import { __ } from '@wordpress/i18n'; */ import TemplatesMenu from './menus/templates'; import TemplatePartsMenu from './menus/template-parts'; +import MainDashboardButton from '../../main-dashboard-button'; import { MENU_ROOT, MENU_TEMPLATE_PARTS, MENU_TEMPLATES } from './constants'; export default function TemplatesNavigation() { diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/utils.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/utils.js new file mode 100644 index 00000000000000..251b90cfe44dfd --- /dev/null +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/utils.js @@ -0,0 +1,11 @@ +/** + * External dependencies + */ +import { deburr } from 'lodash'; + +// @see packages/block-editor/src/components/inserter/search-items.js +export const normalizeInput = ( input ) => + deburr( input ).replace( /^\//, '' ).toLowerCase(); + +export const normalizedSearch = ( title, search ) => + -1 !== normalizeInput( title ).indexOf( normalizeInput( search ) ); diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss index 0b51e8536a2f25..bd94f618b87669 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss @@ -1,15 +1,19 @@ .edit-site-navigation-toggle { + align-items: center; + background: $gray-900; + border-radius: 0; display: none; + position: absolute; + z-index: z-index(".edit-site-navigation-toggle"); + height: $header-height + $border-width; // Cover header border + width: $header-height; @include break-medium() { - align-items: center; - background: $gray-900; - border-radius: 0; display: flex; - position: absolute; - z-index: z-index(".edit-site-navigation-toggle"); - height: $header-height + $border-width; // Cover header border - width: $header-height; + } + + body.is-navigation-sidebar-open & { + display: flex; } } diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 67f6e15128d3a4..939988af3e5340 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -67,4 +67,5 @@ export function initialize( id, settings ) { render( <Editor />, document.getElementById( id ) ); } +export { default as __experimentalMainDashboardButton } from './components/main-dashboard-button'; export { default as __experimentalNavigationToggle } from './components/navigation-sidebar/navigation-toggle'; diff --git a/packages/edit-widgets/src/components/layout/interface.js b/packages/edit-widgets/src/components/layout/interface.js index 9d680a48f3224c..768fc01e6ae407 100644 --- a/packages/edit-widgets/src/components/layout/interface.js +++ b/packages/edit-widgets/src/components/layout/interface.js @@ -4,8 +4,11 @@ import { Button } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { close } from '@wordpress/icons'; -import { __experimentalLibrary as Library } from '@wordpress/block-editor'; -import { useEffect } from '@wordpress/element'; +import { + __experimentalLibrary as Library, + __unstableUseEditorStyles as useEditorStyles, +} from '@wordpress/block-editor'; +import { useEffect, useRef } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { InterfaceSkeleton, ComplementaryArea } from '@wordpress/interface'; import { __ } from '@wordpress/i18n'; @@ -41,6 +44,9 @@ function Interface( { blockEditorSettings } ) { ).getActiveComplementaryArea( 'core/edit-widgets' ), isInserterOpened: !! select( 'core/edit-widgets' ).isInserterOpened(), } ) ); + const ref = useRef(); + + useEditorStyles( ref, blockEditorSettings.styles ); // Inserter and Sidebars are mutually exclusive useEffect( () => { @@ -57,6 +63,7 @@ function Interface( { blockEditorSettings } ) { return ( <InterfaceSkeleton + ref={ ref } labels={ interfaceLabels } header={ <Header /> } secondarySidebar={ diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index 0fbc2959971119..220a558e7fa14d 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -17,7 +17,6 @@ import { useMemo } from '@wordpress/element'; import { BlockEditorProvider, BlockEditorKeyboardShortcuts, - __unstableEditorStyles as EditorStyles, } from '@wordpress/block-editor'; import { ReusableBlocksMenuItems } from '@wordpress/reusable-blocks'; @@ -86,7 +85,6 @@ export default function WidgetAreasBlockEditorProvider( { return ( <> - <EditorStyles styles={ settings.styles } /> <BlockEditorKeyboardShortcuts.Register /> <KeyboardShortcuts.Register /> <SlotFillProvider> diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index afa93a98a3ca77..bd0ffb99021a2a 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -15,7 +15,6 @@ import { EntityProvider } from '@wordpress/core-data'; import { BlockEditorProvider, BlockContextProvider, - __unstableEditorStyles as EditorStyles, } from '@wordpress/block-editor'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; @@ -269,32 +268,29 @@ class EditorProvider extends Component { ); return ( - <> - <EditorStyles styles={ settings.styles } /> - <EntityProvider kind="root" type="site"> - <EntityProvider - kind="postType" - type={ post.type } - id={ post.id } - > - <BlockContextProvider value={ defaultBlockContext }> - <BlockEditorProvider - value={ blocks } - onInput={ resetEditorBlocksWithoutUndoLevel } - onChange={ resetEditorBlocks } - selectionStart={ selectionStart } - selectionEnd={ selectionEnd } - settings={ editorSettings } - useSubRegistry={ false } - > - { children } - <ReusableBlocksMenuItems /> - <ConvertToGroupButtons /> - </BlockEditorProvider> - </BlockContextProvider> - </EntityProvider> + <EntityProvider kind="root" type="site"> + <EntityProvider + kind="postType" + type={ post.type } + id={ post.id } + > + <BlockContextProvider value={ defaultBlockContext }> + <BlockEditorProvider + value={ blocks } + onInput={ resetEditorBlocksWithoutUndoLevel } + onChange={ resetEditorBlocks } + selectionStart={ selectionStart } + selectionEnd={ selectionEnd } + settings={ editorSettings } + useSubRegistry={ false } + > + { children } + <ReusableBlocksMenuItems /> + <ConvertToGroupButtons /> + </BlockEditorProvider> + </BlockContextProvider> </EntityProvider> - </> + </EntityProvider> ); } } diff --git a/packages/env/README.md b/packages/env/README.md index e3533179c196fd..78f94c73c5a7ea 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -328,14 +328,15 @@ You can customize the WordPress installation, plugins and themes that the develo `.wp-env.json` supports six fields for options applicable to both the tests and development instances. -| Field | Type | Default | Description | -| ------------ | -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `"core"` | `string\|null` | `null` | The WordPress installation to use. If `null` is specified, `wp-env` will use the latest production release of WordPress. | -| `"plugins"` | `string[]` | `[]` | A list of plugins to install and activate in the environment. | -| `"themes"` | `string[]` | `[]` | A list of themes to install in the environment. | -| `"port"` | `integer` | `8888` (`8889` for the tests instance) | The primary port number to use for the installation. You'll access the instance through the port: 'http://localhost:8888'. | -| `"config"` | `Object` | See below. | Mapping of wp-config.php constants to their desired values. | -| `"mappings"` | `Object` | `"{}"` | Mapping of WordPress directories to local directories to be mounted in the WordPress instance. | +| Field | Type | Default | Description | +| -------------- | -------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `"core"` | `string\|null` | `null` | The WordPress installation to use. If `null` is specified, `wp-env` will use the latest production release of WordPress. | +| `"phpVersion"` | `string\|null` | `null` | The PHP version to use. If `null` is specified, `wp-env` will use the default version used with production release of WordPress. | +| `"plugins"` | `string[]` | `[]` | A list of plugins to install and activate in the environment. | +| `"themes"` | `string[]` | `[]` | A list of themes to install in the environment. | +| `"port"` | `integer` | `8888` (`8889` for the tests instance) | The primary port number to use for the installation. You'll access the instance through the port: 'http://localhost:8888'. | +| `"config"` | `Object` | See below. | Mapping of wp-config.php constants to their desired values. | +| `"mappings"` | `Object` | `"{}"` | Mapping of WordPress directories to local directories to be mounted in the WordPress instance. | _Note: the port number environment variables (`WP_ENV_PORT` and `WP_ENV_TESTS_PORT`) take precedent over the .wp-env.json values._ @@ -508,4 +509,15 @@ You can tell `wp-env` to use a custom port number so that your instance does not } ``` +#### Specific PHP Version + +You can tell `wp-env` to use a specific PHP version for compatibility and testing. This can also be set via the environment variable `WP_ENV_PHP_VERSION`. + +```json +{ + "phpVersion": "7.2", + "plugins": [ "." ] +} +``` + <br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p> diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js index 54e047fcaff9b0..b569d6df41fd2c 100644 --- a/packages/env/lib/build-docker-compose-config.js +++ b/packages/env/lib/build-docker-compose-config.js @@ -113,6 +113,51 @@ module.exports = function buildDockerComposeConfig( config ) { const developmentPorts = `\${WP_ENV_PORT:-${ config.env.development.port }}:80`; const testsPorts = `\${WP_ENV_TESTS_PORT:-${ config.env.tests.port }}:80`; + // Set the WordPress, WP-CLI, PHPUnit PHP version if defined. + const developmentPhpVersion = config.env.development.phpVersion + ? config.env.development.phpVersion + : ''; + const testsPhpVersion = config.env.tests.phpVersion + ? config.env.tests.phpVersion + : ''; + + // Set the WordPress images with the PHP version tag. + const developmentWpImage = `wordpress${ + developmentPhpVersion ? ':php' + developmentPhpVersion : '' + }`; + const testsWpImage = `wordpress${ + testsPhpVersion ? ':php' + testsPhpVersion : '' + }`; + // Set the WordPress CLI images with the PHP version tag. + const developmentWpCliImage = `wordpress:cli${ + ! developmentPhpVersion || developmentPhpVersion.length === 0 + ? '' + : '-php' + developmentPhpVersion + }`; + const testsWpCliImage = `wordpress:cli${ + ! testsPhpVersion || testsPhpVersion.length === 0 + ? '' + : '-php' + testsPhpVersion + }`; + + // Defaults are to use the most recent version of PHPUnit that provides + // support for the specified version of PHP. + // PHP Unit is assumed to be for Tests so use the testsPhpVersion. + let phpunitTag = 'latest'; + const phpunitPhpVersion = '-php-' + testsPhpVersion + '-fpm'; + if ( testsPhpVersion === '5.6' ) { + phpunitTag = '5' + phpunitPhpVersion; + } else if ( testsPhpVersion === '7.0' ) { + phpunitTag = '6' + phpunitPhpVersion; + } else if ( testsPhpVersion === '7.1' ) { + phpunitTag = '7' + phpunitPhpVersion; + } else if ( [ '7.2', '7.3', '7.4' ].indexOf( testsPhpVersion ) >= 0 ) { + phpunitTag = '8' + phpunitPhpVersion; + } else if ( testsPhpVersion === '8.0' ) { + phpunitTag = '9' + phpunitPhpVersion; + } + const phpunitImage = `wordpressdevelop/phpunit:${ phpunitTag }`; + // The www-data user in wordpress:cli has a different UID (82) to the // www-data user in wordpress (33). Ensure we use the wordpress www-data // user for CLI commands. @@ -132,7 +177,7 @@ module.exports = function buildDockerComposeConfig( config ) { }, wordpress: { depends_on: [ 'mysql' ], - image: 'wordpress', + image: developmentWpImage, ports: [ developmentPorts ], environment: { WORDPRESS_DB_NAME: 'wordpress', @@ -141,7 +186,7 @@ module.exports = function buildDockerComposeConfig( config ) { }, 'tests-wordpress': { depends_on: [ 'mysql' ], - image: 'wordpress', + image: testsWpImage, ports: [ testsPorts ], environment: { WORDPRESS_DB_NAME: 'tests-wordpress', @@ -150,13 +195,13 @@ module.exports = function buildDockerComposeConfig( config ) { }, cli: { depends_on: [ 'wordpress' ], - image: 'wordpress:cli', + image: developmentWpCliImage, volumes: developmentMounts, user: cliUser, }, 'tests-cli': { depends_on: [ 'tests-wordpress' ], - image: 'wordpress:cli', + image: testsWpCliImage, volumes: testsMounts, user: cliUser, }, @@ -165,7 +210,7 @@ module.exports = function buildDockerComposeConfig( config ) { volumes: [ `${ config.configDirectoryPath }:/app` ], }, phpunit: { - image: 'wordpressdevelop/phpunit:${LOCAL_PHP-latest}', + image: phpunitImage, depends_on: [ 'tests-wordpress' ], volumes: [ ...testsMounts, diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 139932fc8ee2e8..2ef2e0d52e70d8 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -175,7 +175,10 @@ module.exports = async function start( { spinner, debug, update } ) { } ); } - spinner.text = 'WordPress started.'; + const siteUrl = config.env.development.config.WP_SITEURL; + spinner.text = 'WordPress started'.concat( + siteUrl ? ` at ${ siteUrl }.` : '.' + ); }; /** diff --git a/packages/env/lib/config/config.js b/packages/env/lib/config/config.js index eac10983632fbf..784ad2e9af2728 100644 --- a/packages/env/lib/config/config.js +++ b/packages/env/lib/config/config.js @@ -26,6 +26,7 @@ const md5 = require( '../md5' ); * @property {boolean} detectedLocalConfig If true, wp-env detected local config and used it. * @property {Object.<string, WPServiceConfig>} env Specific config for different environments. * @property {boolean} debug True if debug mode is enabled. + * @property {string} phpVersion Version of PHP to use in the environments, of the format 0.0. */ /** @@ -69,6 +70,7 @@ module.exports = async function readConfig( configPath ) { // Default configuration which is overridden by .wp-env.json files. const defaultConfiguration = { core: null, + phpVersion: null, plugins: [], themes: [], port: 8888, @@ -253,6 +255,12 @@ function withOverrides( config ) { getNumberFromEnvVariable( 'WP_ENV_TESTS_PORT' ) || config.env.tests.port; + // Override PHP version with environment variable. + config.env.development.phpVersion = + process.env.WP_ENV_PHP_VERSION || config.env.development.phpVersion; + config.env.tests.phpVersion = + process.env.WP_ENV_PHP_VERSION || config.env.tests.phpVersion; + const updateEnvUrl = ( configKey ) => { [ 'development', 'tests' ].forEach( ( envKey ) => { try { diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index a32859b0feeb09..249ca717f79adf 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -35,6 +35,7 @@ const HOME_PATH_PREFIX = `~${ path.sep }`; module.exports = function parseConfig( config, options ) { return { port: config.port, + phpVersion: config.phpVersion, coreSource: includeTestsPath( parseSourceString( config.core, options ), options diff --git a/packages/env/lib/config/test/__snapshots__/config.js.snap b/packages/env/lib/config/test/__snapshots__/config.js.snap index 1420e8ab83f8d1..db2748f02c3329 100644 --- a/packages/env/lib/config/test/__snapshots__/config.js.snap +++ b/packages/env/lib/config/test/__snapshots__/config.js.snap @@ -22,6 +22,7 @@ Object { }, "coreSource": null, "mappings": Object {}, + "phpVersion": null, "pluginSources": Array [], "port": 2000, "themeSources": Array [], @@ -43,6 +44,7 @@ Object { }, "coreSource": null, "mappings": Object {}, + "phpVersion": null, "pluginSources": Array [], "port": 1000, "themeSources": Array [], diff --git a/packages/env/lib/config/validate-config.js b/packages/env/lib/config/validate-config.js index 6a50904cb52f81..c3aa849e6322d1 100644 --- a/packages/env/lib/config/validate-config.js +++ b/packages/env/lib/config/validate-config.js @@ -70,6 +70,19 @@ function validateConfig( config, envLocation ) { ); } } + + if ( + config.phpVersion && + ! ( + typeof config.phpVersion === 'string' && + config.phpVersion.length === 3 + ) + ) { + throw new ValidationError( + `Invalid .wp-env.json: "${ envPrefix }phpVersion" must be a string of the format "0.0".` + ); + } + return config; } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index a977dccfde44cb..794cde5a388552 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Feature + +- Add `no-unsafe-wp-apis` rule to discourage usage of unsafe APIs ([#27301](https://github.com/WordPress/gutenberg/pull/27301)). + ### Documentation - Include a note about the minimum version required for `node` (10.0.0) and `npm` (6.9.0). diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-wp-apis.md b/packages/eslint-plugin/docs/rules/no-unsafe-wp-apis.md new file mode 100644 index 00000000000000..59213648efcab5 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-wp-apis.md @@ -0,0 +1,43 @@ +# Prevent unsafe API usage (no-unsafe-wp-apis) + +Prevent unsafe APIs from `@wordpress/*` packages from being imported. + +This includes experimental and unstable APIs which are expected to change and likely to cause issues in application code. +See the [documentation](https://github.com/WordPress/gutenberg/blob/master/docs/contributors/coding-guidelines.md#experimental-and-unstable-apis). + +> **There is no support commitment for experimental and unstable APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs. +> … +> +> - An **experimental API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion. +> - An **unstable API** is one which serves as a means to an end. It is not desired to ever be converted into a public API. + +## Rule details + +Examples of **incorrect** code for this rule: + +```js +import { __experimentalFeature } from '@wordpress/foo'; +import { __unstableFeature } from '@wordpress/bar'; +``` + +Examples of **correct** code for this rule: + +```js +import { registerBlockType } from '@wordpress/blocks'; +``` + +## Options + +The rule can be configured via an object. +This should be an object where the keys are import package names and the values are arrays of allowed unsafe imports. + +#### Example configuration + +```json +{ + "@wordpress/no-unsafe-wp-apis": [ + "error", + { "@wordpress/block-editor": [ "__experimentalBlock" ] } + ] +} +``` diff --git a/packages/eslint-plugin/rules/__tests__/no-unsafe-wp-apis.js b/packages/eslint-plugin/rules/__tests__/no-unsafe-wp-apis.js new file mode 100644 index 00000000000000..ed7b0ce1684a4b --- /dev/null +++ b/packages/eslint-plugin/rules/__tests__/no-unsafe-wp-apis.js @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +import { RuleTester } from 'eslint'; + +/** + * Internal dependencies + */ +import rule from '../no-unsafe-wp-apis'; + +const ruleTester = new RuleTester( { + parserOptions: { + sourceType: 'module', + ecmaVersion: 6, + }, +} ); + +const options = [ + { '@wordpress/package': [ '__experimentalSafe', '__unstableSafe' ] }, +]; + +ruleTester.run( 'no-unsafe-wp-apis', rule, { + valid: [ + { code: "import _ from 'lodash';", options }, + { code: "import { map } from 'lodash';", options }, + { code: "import { __experimentalFoo } from 'lodash';", options }, + { code: "import { __unstableFoo } from 'lodash';", options }, + { code: "import _, { __unstableFoo } from 'lodash';", options }, + { code: "import * as _ from 'lodash';", options }, + + { code: "import _ from './x';", options }, + { code: "import { map } from './x';", options }, + { code: "import { __experimentalFoo } from './x';", options }, + { code: "import { __unstableFoo } from './x';", options }, + { code: "import _, { __unstableFoo } from './x';", options }, + { code: "import * as _ from './x';", options }, + + { code: "import s from '@wordpress/package';", options }, + { code: "import { feature } from '@wordpress/package';", options }, + { + code: "import { __experimentalSafe } from '@wordpress/package';", + options, + }, + { + code: "import { __unstableSafe } from '@wordpress/package';", + options, + }, + { + code: + "import { feature, __experimentalSafe } from '@wordpress/package';", + options, + }, + { + code: "import s, { __experimentalSafe } from '@wordpress/package';", + options, + }, + { code: "import * as s from '@wordpress/package';", options }, + ], + + invalid: [ + { + code: "import { __experimentalUnsafe } from '@wordpress/package';", + options, + errors: [ + { + message: `Usage of \`__experimentalUnsafe\` from \`@wordpress/package\` is not allowed. +See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`, + type: 'ImportSpecifier', + }, + ], + }, + { + code: "import { __experimentalSafe } from '@wordpress/unsafe';", + options, + errors: [ + { + message: `Usage of \`__experimentalSafe\` from \`@wordpress/unsafe\` is not allowed. +See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`, + type: 'ImportSpecifier', + }, + ], + }, + { + code: + "import { feature, __experimentalSafe } from '@wordpress/unsafe';", + options, + errors: [ + { + message: `Usage of \`__experimentalSafe\` from \`@wordpress/unsafe\` is not allowed. +See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`, + type: 'ImportSpecifier', + }, + ], + }, + { + code: + "import s, { __experimentalUnsafe } from '@wordpress/package';", + options, + errors: [ + { + message: `Usage of \`__experimentalUnsafe\` from \`@wordpress/package\` is not allowed. +See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`, + type: 'ImportSpecifier', + }, + ], + }, + { + code: "import { __unstableFeature } from '@wordpress/package';", + options, + errors: [ + { + message: `Usage of \`__unstableFeature\` from \`@wordpress/package\` is not allowed. +See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`, + type: 'ImportSpecifier', + }, + ], + }, + ], +} ); diff --git a/packages/eslint-plugin/rules/dependency-group.js b/packages/eslint-plugin/rules/dependency-group.js index eaaf716ccade5d..52380418ac520c 100644 --- a/packages/eslint-plugin/rules/dependency-group.js +++ b/packages/eslint-plugin/rules/dependency-group.js @@ -1,7 +1,8 @@ /** @typedef {import('estree').Comment} Comment */ /** @typedef {import('estree').Node} Node */ -module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { meta: { type: 'layout', docs: { @@ -254,4 +255,4 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( { }, }; }, -} ); +}; diff --git a/packages/eslint-plugin/rules/no-unsafe-wp-apis.js b/packages/eslint-plugin/rules/no-unsafe-wp-apis.js new file mode 100644 index 00000000000000..65bcc2170d45ba --- /dev/null +++ b/packages/eslint-plugin/rules/no-unsafe-wp-apis.js @@ -0,0 +1,88 @@ +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + type: 'problem', + meta: { + schema: [ + { + type: 'object', + additionalProperties: false, + patternProperties: { + '^@wordpress\\/[a-zA-Z0-9_-]+$': { + type: 'array', + uniqueItems: true, + minItems: 1, + items: { + type: 'string', + pattern: '^(?:__experimental|__unstable)', + }, + }, + }, + }, + ], + }, + create( context ) { + /** @type {AllowedImportsMap} */ + const allowedImports = + ( context.options && + typeof context.options[ 0 ] === 'object' && + context.options[ 0 ] ) || + {}; + const reporter = makeListener( { allowedImports, context } ); + + return { ImportDeclaration: reporter }; + }, +}; + +/** + * @param {Object} _ + * @param {AllowedImportsMap} _.allowedImports + * @param {import('eslint').Rule.RuleContext} _.context + * + * @return {(node: Node) => void} Listener function + */ +function makeListener( { allowedImports, context } ) { + return function reporter( node ) { + if ( node.type !== 'ImportDeclaration' ) { + return; + } + if ( typeof node.source.value !== 'string' ) { + return; + } + + const sourceModule = node.source.value.trim(); + + // Ignore non-WordPress packages + if ( ! sourceModule.startsWith( '@wordpress/' ) ) { + return; + } + + const allowedImportNames = allowedImports[ sourceModule ] || []; + + node.specifiers.forEach( ( specifierNode ) => { + if ( specifierNode.type !== 'ImportSpecifier' ) { + return; + } + + const importedName = specifierNode.imported.name; + + if ( + ! importedName.startsWith( '__unstable' ) && + ! importedName.startsWith( '__experimental' ) + ) { + return; + } + + if ( allowedImportNames.includes( importedName ) ) { + return; + } + + context.report( { + message: `Usage of \`${ importedName }\` from \`${ sourceModule }\` is not allowed.\nSee https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`, + node: specifierNode, + } ); + } ); + }; +} + +/** @typedef {import('estree').Node} Node */ +/** @typedef {Record<string, string[]|undefined>} AllowedImportsMap */ diff --git a/packages/eslint-plugin/tsconfig.json b/packages/eslint-plugin/tsconfig.json index 0f0a4598184f80..d292d97510c414 100644 --- a/packages/eslint-plugin/tsconfig.json +++ b/packages/eslint-plugin/tsconfig.json @@ -1,13 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "module": "CommonJS", "rootDir": "rules", "declarationDir": "build-types" }, // NOTE: This package is being progressively typed. You are encouraged to // expand this array with files which can be type-checked. At some point in // the future, this can be simplified to an `includes` of `src/**/*`. - "files": [ - "rules/dependency-group.js" - ] + "files": [ "rules/dependency-group.js", "rules/no-unsafe-wp-apis.js" ] } diff --git a/packages/interface/src/components/index.js b/packages/interface/src/components/index.js index d8ba05d8230dc2..971b42522ae3c4 100644 --- a/packages/interface/src/components/index.js +++ b/packages/interface/src/components/index.js @@ -3,5 +3,4 @@ export { default as ComplementaryAreaMoreMenuItem } from './complementary-area-m export { default as FullscreenMode } from './fullscreen-mode'; export { default as InterfaceSkeleton } from './interface-skeleton'; export { default as PinnedItems } from './pinned-items'; -export { default as __experimentalMainDashboardButton } from './main-dashboard-button'; export { default as ActionItem } from './action-item'; diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index cd9dcc79ed2b9b..f912598e0f498b 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -83,6 +83,7 @@ enum MediaType { VIDEO("video"), MEDIA("media"), AUDIO("audio"), + ANY("any"), OTHER("other"); String name; diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 637ce489ea6044..b523510db89c4f 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -138,6 +138,7 @@ public interface OnMediaLibraryButtonListener { void onMediaLibraryImageButtonClicked(boolean allowMultipleSelection); void onMediaLibraryVideoButtonClicked(boolean allowMultipleSelection); void onMediaLibraryMediaButtonClicked(boolean allowMultipleSelection); + void onMediaLibraryFileButtonClicked(boolean allowMultipleSelection); void onUploadPhotoButtonClicked(boolean allowMultipleSelection); void onCapturePhotoButtonClicked(); void onUploadVideoButtonClicked(boolean allowMultipleSelection); @@ -244,6 +245,8 @@ public void requestMediaPickFromMediaLibrary(MediaSelectedCallback mediaSelected mOnMediaLibraryButtonListener.onMediaLibraryVideoButtonClicked(allowMultipleSelection); } else if (mediaType == MediaType.MEDIA) { mOnMediaLibraryButtonListener.onMediaLibraryMediaButtonClicked(allowMultipleSelection); + } else if (mediaType == MediaType.ANY) { + mOnMediaLibraryButtonListener.onMediaLibraryFileButtonClicked(allowMultipleSelection); } } diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index 7a194e53e31a54..b52699d7ce921c 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -258,6 +258,7 @@ extension Gutenberg { case video case audio case other + case any } } diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java index 3c5fa9517cb14d..628feb8b8b6dbb 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java @@ -72,10 +72,18 @@ public void requestMediaPickFromDeviceLibrary(MediaSelectedCallback mediaSelecte @Override public void requestMediaPickFromMediaLibrary(MediaSelectedCallback mediaSelectedCallback, Boolean allowMultipleSelection, MediaType mediaType) { List<RNMedia> rnMediaList = new ArrayList<>(); - if (mediaType == MediaType.IMAGE) { - rnMediaList.add(new Media(1, "https://cldup.com/cXyG__fTLN.jpg", "image", "Mountain","")); - } else if (mediaType == MediaType.VIDEO) { - rnMediaList.add(new Media(2, "https://i.cloudup.com/YtZFJbuQCE.mov", "video", "Cloudup","" )); + + switch (mediaType) { + case IMAGE: + rnMediaList.add(new Media(1, "https://cldup.com/cXyG__fTLN.jpg", "image", "Mountain", "")); + break; + case VIDEO: + rnMediaList.add(new Media(2, "https://i.cloudup.com/YtZFJbuQCE.mov", "video", "Cloudup", "")); + case ANY: + case OTHER: + rnMediaList.add(new Media(3, "https://wordpress.org/latest.zip", "zip", "WordPress latest version", "WordPress.zip")); + break; + } mediaSelectedCallback.onMediaFileSelected(rnMediaList); } @@ -111,7 +119,7 @@ public void editorDidAutosave() { @Override public void getOtherMediaPickerOptions(OtherMediaOptionsReceivedCallback otherMediaOptionsReceivedCallback, MediaType mediaType) { - if (mediaType == MediaType.OTHER) { + if (mediaType == MediaType.ANY) { ArrayList<MediaOption> mediaOptions = new ArrayList<>(); mediaOptions.add(new MediaOption("1", "Choose from device")); otherMediaOptionsReceivedCallback.onOtherMediaOptionsReceived(mediaOptions); diff --git a/packages/react-native-editor/ios/DocumentsMediaSource.swift b/packages/react-native-editor/ios/DocumentsMediaSource.swift index 5e352ea9b68c6b..5914cf65e0134b 100644 --- a/packages/react-native-editor/ios/DocumentsMediaSource.swift +++ b/packages/react-native-editor/ios/DocumentsMediaSource.swift @@ -65,8 +65,10 @@ extension Gutenberg.MediaType { return String(kUTTypeMovie) case .audio: return String(kUTTypeAudio) - case .other: - return String(kUTTypeItem) - } + case .any: + return String(kUTTypeItem) + case .other: + return nil + } } } diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index 3f393e60b329dc..bb1ee8618f2e94 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -100,6 +100,8 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } else { callback([MediaInfo(id: 2, url: "https://i.cloudup.com/YtZFJbuQCE.mov", type: "video", caption: "Cloudup")]) } + case .other, .any: + callback([MediaInfo(id: 1, url: "https://wordpress.org/latest.zip", type: "zip", caption: "WordPress latest version", title: "WordPress.zip")]) default: break } @@ -110,7 +112,7 @@ extension GutenbergViewController: GutenbergBridgeDelegate { print("Gutenberg did request a device media picker, opening the camera picker") pickAndUpload(from: .camera, filter: currentFilter, callback: callback) - case .filesApp: + case .filesApp, .otherApps: pickAndUploadFromFilesApp(filter: currentFilter, callback: callback) default: break } @@ -311,12 +313,13 @@ extension GutenbergViewController: GutenbergBridgeDataSource { } func gutenbergMediaSources() -> [Gutenberg.MediaSource] { - return [.filesApp] + return [.filesApp, .otherApps] } } extension Gutenberg.MediaSource { - static let filesApp = Gutenberg.MediaSource(id: "files-app", label: "Pick a file", types: [.image, .video, .audio, .other]) + static let filesApp = Gutenberg.MediaSource(id: "files-app", label: "Choose from device", types: [.any]) + static let otherApps = Gutenberg.MediaSource(id: "other-apps", label: "Other Apps", types: [.image, .video, .audio, .other]) } //MARK: - Navigation bar diff --git a/packages/react-native-editor/ios/GutenbergDemo/MediaUploadCoordinator.swift b/packages/react-native-editor/ios/GutenbergDemo/MediaUploadCoordinator.swift index 9571d532d92c87..2655af936b466b 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/MediaUploadCoordinator.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/MediaUploadCoordinator.swift @@ -20,6 +20,7 @@ class MediaUploadCoordinator: NSObject { func upload(url: URL) -> Int32? { //Make sure the media is not larger than a 32 bits to number to avoid problems when bridging to JS + successfullUpload = true let mediaID = Int32(truncatingIfNeeded:UUID().uuidString.hash) let progress = Progress(parent: nil, userInfo: [ProgressUserInfoKey.mediaID: mediaID, ProgressUserInfoKey.mediaURL: url]) progress.totalUnitCount = 100 diff --git a/packages/reusable-blocks/README.md b/packages/reusable-blocks/README.md index 8d6848c93e041c..140e18ef70ed9b 100644 --- a/packages/reusable-blocks/README.md +++ b/packages/reusable-blocks/README.md @@ -72,8 +72,10 @@ return ( This package also provides convenient utilities for managing reusable blocks through redux actions: ```js +import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; + function MyConvertToStaticButton( { clientId } ) { - const { __experimentalConvertBlockToStatic } = useDispatch( 'core/reusable-blocks' ); + const { __experimentalConvertBlockToStatic } = useDispatch( reusableBlocksStore ); return ( <button onClick={() => __experimentalConvertBlockToStatic( clientId )} > Convert to static @@ -82,7 +84,7 @@ function MyConvertToStaticButton( { clientId } ) { } function MyConvertToReusableButton( { clientId } ) { - const { __experimentalConvertBlocksToReusable } = useDispatch( 'core/reusable-blocks' ); + const { __experimentalConvertBlocksToReusable } = useDispatch( reusableBlocksStore ); return ( <button onClick={() => __experimentalConvertBlocksToReusable( [ clientId ] )} > Convert to reusable @@ -91,7 +93,7 @@ function MyConvertToReusableButton( { clientId } ) { } function MyDeleteReusableBlockButton( { id } ) { - const { __experimentalDeleteReusableBlock } = useDispatch( 'core/reusable-blocks' ); + const { __experimentalDeleteReusableBlock } = useDispatch( reusableBlocksStore ); return ( <button onClick={() => __experimentalDeleteReusableBlock( id )} > Delete reusable block diff --git a/packages/reusable-blocks/src/store/controls.js b/packages/reusable-blocks/src/store/controls.js index 8b9b9613c05217..308cb68934338c 100644 --- a/packages/reusable-blocks/src/store/controls.js +++ b/packages/reusable-blocks/src/store/controls.js @@ -10,6 +10,11 @@ import { import { createRegistryControl } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { store as reusableBlocksStore } from './index.js'; + /** * Convert a reusable block to a static block effect handler * @@ -94,7 +99,7 @@ const controls = { .dispatch( 'core/block-editor' ) .replaceBlocks( clientIds, newBlock ); registry - .dispatch( 'core/reusable-blocks' ) + .dispatch( reusableBlocksStore ) .__experimentalSetEditingReusableBlock( newBlock.clientId, true diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index e7d6b6cbb577d1..625da9012fd05c 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -374,7 +374,7 @@ function RichText( } if ( onPaste ) { - const files = [ ...getFilesFromDataTransfer( clipboardData ) ]; + const files = getFilesFromDataTransfer( clipboardData ); onPaste( { value: removeEditorOnlyFormats( record.current ), diff --git a/phpunit/class-theme-json-legacy-settings-test.php b/phpunit/class-wp-theme-json-legacy-settings-test.php similarity index 100% rename from phpunit/class-theme-json-legacy-settings-test.php rename to phpunit/class-wp-theme-json-legacy-settings-test.php diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 8682d0618ae546..341fa24d58a83a 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -215,10 +215,18 @@ function test_get_stylesheet() { ) ); - $result = $theme_json->get_stylesheet(); - $stylesheet = ':root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.has-grey-color{color: grey;}.has-grey-background-color{background-color: grey;}'; - - $this->assertEquals( $stylesheet, $result ); + $this->assertEquals( + $theme_json->get_stylesheet(), + ':root{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}:root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);}.has-grey-color{color: grey;}.has-grey-background-color{background-color: grey;}' + ); + $this->assertEquals( + $theme_json->get_stylesheet( 'block_styles' ), + ':root{--wp--style--color--link: #111;color: var(--wp--preset--color--grey);}.has-grey-color{color: grey;}.has-grey-background-color{background-color: grey;}' + ); + $this->assertEquals( + $theme_json->get_stylesheet( 'css_variables' ), + ':root{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}' + ); } public function test_merge_incoming_data() { diff --git a/test/native/__mocks__/styleMock.js b/test/native/__mocks__/styleMock.js index de672610c66eb0..3022d6d1e59772 100644 --- a/test/native/__mocks__/styleMock.js +++ b/test/native/__mocks__/styleMock.js @@ -96,4 +96,10 @@ module.exports = { scrollableContent: { paddingBottom: 20, }, + buttonText: { + color: 'white', + }, + placeholderTextColor: { + color: 'white', + }, };