diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index df11d86f673f88..0f40c8fce11d68 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -64,6 +64,8 @@ jobs: old_version: ${{ steps.get_version.outputs.old_version }} new_version: ${{ steps.get_version.outputs.new_version }} release_branch: ${{ steps.get_version.outputs.release_branch }} + release_branch_commit: ${{ steps.commit_version_bump_to_release_branch.outputs.version_bump_commit }} + trunk_commit: ${{ steps.commit_version_bump_to_trunk.outputs.version_bump_commit }} steps: - name: Checkout code @@ -125,16 +127,19 @@ jobs: sed -i "s/${{ steps.get_version.outputs.old_version }}/${VERSION}/g" readme.txt - name: Commit the version bump to the release branch + id: commit_version_bump_to_release_branch run: | git add gutenberg.php package.json package-lock.json readme.txt git commit -m "Bump plugin version to ${{ steps.get_version.outputs.new_version }}" git push --set-upstream origin "${{ steps.get_version.outputs.release_branch }}" + echo "::set-output name=version_bump_commit::$(git rev-parse --verify --short HEAD)" - name: Fetch trunk if: ${{ github.ref != 'refs/heads/trunk' }} run: git fetch --depth=1 origin trunk - name: Cherry-pick the version bump commit to trunk + id: commit_version_bump_to_trunk run: | git checkout trunk git pull @@ -142,6 +147,7 @@ jobs: if [[ ${{ steps.get_version.outputs.old_version }} == "$TRUNK_VERSION" ]]; then git cherry-pick "${{ steps.get_version.outputs.release_branch }}" git push + echo "::set-output name=version_bump_commit::$(git rev-parse --verify --short HEAD)" fi build: @@ -154,6 +160,8 @@ jobs: github.event_name == 'workflow_dispatch' || github.repository == 'WordPress/gutenberg' ) + outputs: + job_status: ${{ job.status }} steps: - name: Checkout code @@ -201,6 +209,51 @@ jobs: name: release-notes path: ./release-notes.txt + revert-version-bump: + name: Revert version bump if build failed + needs: [bump-version, build] + runs-on: ubuntu-latest + if: | + always() && + ( needs.build.outputs.job_status == 'failure' ) && + needs.bump-version.outputs.release_branch_commit + + steps: + - name: Checkout code + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + with: + fetch-depth: 2 + ref: ${{ needs.bump-version.outputs.release_branch }} + token: ${{ secrets.GUTENBERG_TOKEN }} + + - name: Configure git user name and email + run: | + git config user.name "Gutenberg Repository Automation" + git config user.email gutenberg@wordpress.org + + - name: Revert version bump commit on release branch + if: | + github.event.inputs.version == 'stable' || + contains( needs.bump-version.outputs.old_version, 'rc' ) + run: | + git revert --no-edit ${{ needs.bump-version.outputs.release_branch_commit }} + git push --set-upstream origin "${{ needs.bump-version.outputs.release_branch }}" + + - name: Delete release branch if it was only just created for the RC + if: | + github.event.inputs.version == 'rc' && + ! contains( needs.bump-version.outputs.old_version, 'rc' ) + run: | + git push origin :"${{ needs.bump-version.outputs.release_branch }}" + + - name: Revert version bump on trunk + if: ${{ needs.bump-version.outputs.trunk_commit }} + run: | + git fetch --depth=2 origin trunk + git checkout trunk + git revert --no-edit ${{ needs.bump-version.outputs.trunk_commit }} + git push --set-upstream origin trunk + create-release: name: Create Release Draft and Attach Asset needs: [bump-version, build] diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index 4de60b55130c95..fa587db0a7b95f 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -14,11 +14,11 @@ concurrency: jobs: test: - runs-on: macos-latest + runs-on: macos-11 if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: matrix: - xcode: [12.2] + xcode: [12.5.1] native-test-name: [gutenberg-editor-initial-html] node: ['14'] diff --git a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md index c7e28057671701..decbb981fa20a8 100644 --- a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md +++ b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md @@ -79,13 +79,13 @@ const ALLOWED_BLOCKS = [ 'core/image', 'core/paragraph' ]; ## Orientation -By default, `InnerBlocks` expects its blocks to be shown in a vertical list. A valid use-case is to style InnerBlocks to appear horizontally. When blocks are styled in such a way, the `orientation` prop can be used to indicate a horizontal layout: +By default, `InnerBlocks` expects its blocks to be shown in a vertical list. A valid use-case is to style inner blocks to appear horizontally, for instance by adding CSS flex or grid properties to the inner blocks wrapper. When blocks are styled in such a way, the `orientation` prop can be set to indicate that a horizontal layout is being used: ```js ``` -Specifying this prop will result in the block movers being shown horizontally, and also ensure drag and drop works correctly. +Specifying this prop does not affect the layout of the inner blocks, but results in the block mover icons in the child blocks being displayed horizontally, and also ensures that drag and drop works correctly. ## Template diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md index c4634acbbbfdc2..0eb28f9a29e0e4 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/theme-json.md @@ -965,6 +965,15 @@ Currently block variations exist for "header" and "footer" values of the area te } ``` +## Developing with theme.json + +It can be difficult to remember the theme.json settings and properties while you develop, so a JSON scheme was created to help. The schema is available at [SchemaStore.org](https://schemastore.org/) + +To use the schema, add `"$schema": "https://json.schemastore.org/theme-v1.json"` to the beginning of your theme.json file. Visual Studio Code and other editors will pick up the schema and can provide help like tooltips, autocomplete, or schema validation in the editor. + +![Example using validation with schema](https://developer.wordpress.org/files/2021/10/schema-validation.gif) + + ## Frequently Asked Questions ### The naming schema of CSS Custom Properties diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 3b7ebb61d45136..a8bee440a0c99f 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -750,29 +750,23 @@ public function prepare_item_for_response( $post, $request ) { */ protected function prepare_links( $menu_item ) { $links = parent::prepare_links( $menu_item ); + if ( empty( $menu_item->object_id ) ) { + return $links; + } - if ( 'post_type' === $menu_item->type && ! empty( $menu_item->object_id ) ) { - $post_type_object = get_post_type_object( $menu_item->object ); - if ( $post_type_object->show_in_rest ) { - $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; - $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) ); - $links['https://api.w.org/object'][] = array( - 'href' => $url, - 'post_type' => $menu_item->type, - 'embeddable' => true, - ); - } - } elseif ( 'taxonomy' === $menu_item->type && ! empty( $menu_item->object_id ) ) { - $taxonomy_object = get_taxonomy( $menu_item->object ); - if ( $taxonomy_object->show_in_rest ) { - $rest_base = ! empty( $taxonomy_object->rest_base ) ? $taxonomy_object->rest_base : $taxonomy_object->name; - $url = rest_url( sprintf( 'wp/v2/%s/%d', $rest_base, $menu_item->object_id ) ); - $links['https://api.w.org/object'][] = array( - 'href' => $url, - 'taxonomy' => $menu_item->type, - 'embeddable' => true, - ); - } + $path = ''; + if ( 'post_type' === $menu_item->type ) { + $path = rest_get_route_for_post( $menu_item->object_id ); + } elseif ( 'taxonomy' === $menu_item->type ) { + $path = rest_get_route_for_term( $menu_item->object_id ); + } + + if ( $path ) { + $links['https://api.w.org/object'][] = array( + 'href' => rest_url( $path ), + 'post_type' => $menu_item->type, + 'embeddable' => true, + ); } return $links; diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index 7f5eb1f09527d6..c4d1dfcf9723c9 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -274,6 +274,18 @@ public static function get_theme_data( $theme_support_data = array() ) { $theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json' ) ); $theme_json_data = self::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) ); self::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data ); + + if ( wp_get_theme()->parent() ) { + // Get parent theme.json. + $parent_theme_json_data = self::read_json_file( self::get_file_path_from_theme( 'theme.json', true ) ); + $parent_theme_json_data = self::translate( $parent_theme_json_data, wp_get_theme()->parent()->get( 'TextDomain' ) ); + $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); + + // Merge the child theme.json into the parent theme.json. + // The child theme takes precedence over the parent. + $parent_theme->merge( self::$theme ); + self::$theme = $parent_theme; + } } if ( empty( $theme_support_data ) ) { @@ -476,7 +488,7 @@ public static function get_user_custom_post_type_id() { */ public static function theme_has_support() { if ( ! isset( self::$theme_has_support ) ) { - self::$theme_has_support = (bool) self::get_file_path_from_theme( 'theme.json' ); + self::$theme_has_support = is_readable( get_theme_file_path( 'theme.json' ) ); } return self::$theme_has_support; @@ -490,15 +502,14 @@ public static function theme_has_support() { * otherwise returns the whole file path. * * @param string $file_name Name of the file. + * @param bool $template Use template theme directroy. Default: false. * @return string The whole file path or empty if the file doesn't exist. */ - private static function get_file_path_from_theme( $file_name ) { - $located = ''; - $candidate = get_theme_file_path( $file_name ); - if ( is_readable( $candidate ) ) { - $located = $candidate; - } - return $located; + private static function get_file_path_from_theme( $file_name, $template = false ) { + $path = $template ? get_template_directory() : get_stylesheet_directory(); + $candidate = $path . '/' . $file_name; + + return is_readable( $candidate ) ? $candidate : ''; } /** diff --git a/lib/editor-settings.php b/lib/editor-settings.php index 7e09cd52e6e228..db5ae81802815e 100644 --- a/lib/editor-settings.php +++ b/lib/editor-settings.php @@ -75,6 +75,18 @@ function gutenberg_initialize_editor( $editor_name, $editor_script_handle, $sett 'rest_preload_api_request', array() ); + + /** + * Filters the array of data that has been preloaded. + * + * The dynamic portion of the hook name, `$editor_name`, refers to the type of block editor. + * + * @param array $preload_data Array containing the preloaded data. + * @param string $editor_name Current editor name. + * @param array Array containing the filtered preloaded data. + */ + $preload_data = apply_filters( 'block_editor_preload_data', $preload_data, $editor_name ); + wp_add_inline_script( 'wp-api-fetch', sprintf( diff --git a/lib/navigation-page.php b/lib/navigation-page.php index 568fca8cb3bbfa..959c5d73864760 100644 --- a/lib/navigation-page.php +++ b/lib/navigation-page.php @@ -21,23 +21,84 @@ class="edit-navigation" } /** - * Initialize the Gutenberg Navigation page. + * This function returns an url for the /__experimental/menus endpoint * - * @since 7.8.0 + * @since 11.8.0 + * + * @param int $results_per_page Results per page. + * @return string + */ +function gutenberg_navigation_get_menus_endpoint( $results_per_page = 100 ) { + return '/__experimental/menus?' . build_query( + array( + 'per_page' => $results_per_page, + 'context' => 'edit', + '_locale' => 'user', + ) + ); +} + +/** + * This function returns an url for the /__experimental/menu-items endpoint + * + * @since 11.8.0 + * + * @param int $menu_id Menu ID. + * @param int $results_per_page Results per page. + * @return string + */ +function gutenberg_navigation_get_menu_items_endpoint( $menu_id, $results_per_page = 100 ) { + return '/__experimental/menu-items?' . build_query( + array( + 'context' => 'edit', + 'menus' => $menu_id, + 'per_page' => $results_per_page, + '_locale' => 'user', + ) + ); +} + +/** + * This function returns an url for the /wp/v2/types endpoint + * + * @since 11.8.0 + * + * @return string + */ +function gutenberg_navigation_get_types_endpoint() { + return '/wp/v2/types?' . build_query( + array( + 'context' => 'edit', + ) + ); +} + +/** + * Initialize the Gutenberg Navigation page. * * @param string $hook Page. + * @since 7.8.0 */ function gutenberg_navigation_init( $hook ) { if ( 'gutenberg_page_gutenberg-navigation' !== $hook ) { - return; + return; } + $menus = wp_get_nav_menus(); + $first_menu_id = ! empty( $menus ) ? $menus[0]->term_id : null; + $preload_paths = array( '/__experimental/menu-locations', array( '/wp/v2/pages', 'OPTIONS' ), array( '/wp/v2/posts', 'OPTIONS' ), + gutenberg_navigation_get_menus_endpoint(), + gutenberg_navigation_get_types_endpoint(), ); + if ( $first_menu_id ) { + $preload_paths[] = gutenberg_navigation_get_menu_items_endpoint( $first_menu_id ); + } + $settings = array_merge( gutenberg_get_default_block_editor_settings(), array( @@ -82,3 +143,41 @@ function gutenberg_navigation_editor_load_block_editor_scripts_and_styles( $is_b } add_filter( 'should_load_block_editor_scripts_and_styles', 'gutenberg_navigation_editor_load_block_editor_scripts_and_styles' ); + +/** + * This function removes menu-related data from the "common" preloading middleware and calls + * createMenuPreloadingMiddleware middleware because we need to use custom preloading logic for menus. + * + * @param Array $preload_data Array containing the preloaded data. + * @param string $context Current editor name. + * @return array Filtered preload data. + */ +function gutenberg_navigation_editor_preload_menus( $preload_data, $context ) { + if ( 'navigation_editor' !== $context ) { + return $preload_data; + } + + $menus_data_path = gutenberg_navigation_get_menus_endpoint(); + $menus_data = array(); + if ( ! empty( $preload_data[ $menus_data_path ] ) ) { + $menus_data = array( $menus_data_path => $preload_data[ $menus_data_path ] ); + } + + if ( ! $menus_data ) { + return $preload_data; + } + + wp_add_inline_script( + 'wp-edit-navigation', + sprintf( + 'wp.apiFetch.use( wp.editNavigation.__unstableCreateMenuPreloadingMiddleware( %s ) );', + wp_json_encode( $menus_data ) + ), + 'after' + ); + + unset( $preload_data[ $menus_data_path ] ); + return $preload_data; +} + +add_filter( 'block_editor_preload_data', 'gutenberg_navigation_editor_preload_menus', 10, 2 ); diff --git a/package-lock.json b/package-lock.json index 97de983112cf1b..235b750396072f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7995,6 +7995,339 @@ } } }, + "@storybook/addon-controls": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-6.3.12.tgz", + "integrity": "sha512-WO/PbygE4sDg3BbstJ49q0uM3Xu5Nw4lnHR5N4hXSvRAulZt1d1nhphRTHjfX+CW+uBcfzkq9bksm6nKuwmOyw==", + "dev": true, + "requires": { + "@storybook/addons": "6.3.12", + "@storybook/api": "6.3.12", + "@storybook/client-api": "6.3.12", + "@storybook/components": "6.3.12", + "@storybook/node-logger": "6.3.12", + "@storybook/theming": "6.3.12", + "core-js": "^3.8.2", + "ts-dedent": "^2.0.0" + }, + "dependencies": { + "@emotion/styled": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.0.27.tgz", + "integrity": "sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==", + "dev": true, + "requires": { + "@emotion/styled-base": "^10.0.27", + "babel-plugin-emotion": "^10.0.27" + } + }, + "@storybook/addons": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.12.tgz", + "integrity": "sha512-UgoMyr7Qr0FS3ezt8u6hMEcHgyynQS9ucr5mAwZky3wpXRPFyUTmMto9r4BBUdqyUvTUj/LRKIcmLBfj+/l0Fg==", + "dev": true, + "requires": { + "@storybook/api": "6.3.12", + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/router": "6.3.12", + "@storybook/theming": "6.3.12", + "core-js": "^3.8.2", + "global": "^4.4.0", + "regenerator-runtime": "^0.13.7" + } + }, + "@storybook/api": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.3.12.tgz", + "integrity": "sha512-LScRXUeCWEW/OP+jiooNMQICVdusv7azTmULxtm72fhkXFRiQs2CdRNTiqNg46JLLC9z95f1W+pGK66X6HiiQA==", + "dev": true, + "requires": { + "@reach/router": "^1.3.4", + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/csf": "0.0.1", + "@storybook/router": "6.3.12", + "@storybook/semver": "^7.3.2", + "@storybook/theming": "6.3.12", + "@types/reach__router": "^1.3.7", + "core-js": "^3.8.2", + "fast-deep-equal": "^3.1.3", + "global": "^4.4.0", + "lodash": "^4.17.20", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "regenerator-runtime": "^0.13.7", + "store2": "^2.12.0", + "telejson": "^5.3.2", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/channel-postmessage": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.12.tgz", + "integrity": "sha512-Ou/2Ga3JRTZ/4sSv7ikMgUgLTeZMsXXWLXuscz4oaYhmOqAU9CrJw0G1NitwBgK/+qC83lEFSLujHkWcoQDOKg==", + "dev": true, + "requires": { + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", + "core-js": "^3.8.2", + "global": "^4.4.0", + "qs": "^6.10.0", + "telejson": "^5.3.2" + } + }, + "@storybook/channels": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.12.tgz", + "integrity": "sha512-l4sA+g1PdUV8YCbgs47fIKREdEQAKNdQIZw0b7BfTvY9t0x5yfBywgQhYON/lIeiNGz2OlIuD+VUtqYfCtNSyw==", + "dev": true, + "requires": { + "core-js": "^3.8.2", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/client-api": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.12.tgz", + "integrity": "sha512-xnW+lKKK2T774z+rOr9Wopt1aYTStfb86PSs9p3Fpnc2Btcftln+C3NtiHZl8Ccqft8Mz/chLGgewRui6tNI8g==", + "dev": true, + "requires": { + "@storybook/addons": "6.3.12", + "@storybook/channel-postmessage": "6.3.12", + "@storybook/channels": "6.3.12", + "@storybook/client-logger": "6.3.12", + "@storybook/core-events": "6.3.12", + "@storybook/csf": "0.0.1", + "@types/qs": "^6.9.5", + "@types/webpack-env": "^1.16.0", + "core-js": "^3.8.2", + "global": "^4.4.0", + "lodash": "^4.17.20", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "regenerator-runtime": "^0.13.7", + "stable": "^0.1.8", + "store2": "^2.12.0", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/client-logger": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.12.tgz", + "integrity": "sha512-zNDsamZvHnuqLznDdP9dUeGgQ9TyFh4ray3t1VGO7ZqWVZ2xtVCCXjDvMnOXI2ifMpX5UsrOvshIPeE9fMBmiQ==", + "dev": true, + "requires": { + "core-js": "^3.8.2", + "global": "^4.4.0" + } + }, + "@storybook/components": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.3.12.tgz", + "integrity": "sha512-kdQt8toUjynYAxDLrJzuG7YSNL6as1wJoyzNUaCfG06YPhvIAlKo7le9tS2mThVFN5e9nbKrW3N1V1sp6ypZXQ==", + "dev": true, + "requires": { + "@popperjs/core": "^2.6.0", + "@storybook/client-logger": "6.3.12", + "@storybook/csf": "0.0.1", + "@storybook/theming": "6.3.12", + "@types/color-convert": "^2.0.0", + "@types/overlayscrollbars": "^1.12.0", + "@types/react-syntax-highlighter": "11.0.5", + "color-convert": "^2.0.1", + "core-js": "^3.8.2", + "fast-deep-equal": "^3.1.3", + "global": "^4.4.0", + "lodash": "^4.17.20", + "markdown-to-jsx": "^7.1.3", + "memoizerific": "^1.11.3", + "overlayscrollbars": "^1.13.1", + "polished": "^4.0.5", + "prop-types": "^15.7.2", + "react-colorful": "^5.1.2", + "react-popper-tooltip": "^3.1.1", + "react-syntax-highlighter": "^13.5.3", + "react-textarea-autosize": "^8.3.0", + "regenerator-runtime": "^0.13.7", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/core-events": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.3.12.tgz", + "integrity": "sha512-SXfD7xUUMazaeFkB92qOTUV8Y/RghE4SkEYe5slAdjeocSaH7Nz2WV0rqNEgChg0AQc+JUI66no8L9g0+lw4Gw==", + "dev": true, + "requires": { + "core-js": "^3.8.2" + } + }, + "@storybook/node-logger": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.3.12.tgz", + "integrity": "sha512-iktOem/Ls2+dsZY9PhPeC6T1QhX/y7OInP88neLsqEPEbB2UXca3Ydv7OZBhBVbvN25W45b05MRzbtNUxYLNRw==", + "dev": true, + "requires": { + "@types/npmlog": "^4.1.2", + "chalk": "^4.1.0", + "core-js": "^3.8.2", + "npmlog": "^4.1.2", + "pretty-hrtime": "^1.0.3" + } + }, + "@storybook/router": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.3.12.tgz", + "integrity": "sha512-G/pNGCnrJRetCwyEZulHPT+YOcqEj/vkPVDTUfii2qgqukup6K0cjwgd7IukAURnAnnzTi1gmgFuEKUi8GE/KA==", + "dev": true, + "requires": { + "@reach/router": "^1.3.4", + "@storybook/client-logger": "6.3.12", + "@types/reach__router": "^1.3.7", + "core-js": "^3.8.2", + "fast-deep-equal": "^3.1.3", + "global": "^4.4.0", + "lodash": "^4.17.20", + "memoizerific": "^1.11.3", + "qs": "^6.10.0", + "ts-dedent": "^2.0.0" + } + }, + "@storybook/semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==", + "dev": true, + "requires": { + "core-js": "^3.6.5", + "find-up": "^4.1.0" + } + }, + "@storybook/theming": { + "version": "6.3.12", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.3.12.tgz", + "integrity": "sha512-wOJdTEa/VFyFB2UyoqyYGaZdym6EN7RALuQOAMT6zHA282FBmKw8nL5DETHEbctpnHdcrMC/391teK4nNSrdOA==", + "dev": true, + "requires": { + "@emotion/core": "^10.1.1", + "@emotion/is-prop-valid": "^0.8.6", + "@emotion/styled": "^10.0.27", + "@storybook/client-logger": "6.3.12", + "core-js": "^3.8.2", + "deep-object-diff": "^1.1.0", + "emotion-theming": "^10.0.27", + "global": "^4.4.0", + "memoizerific": "^1.11.3", + "polished": "^4.0.5", + "resolve-from": "^5.0.0", + "ts-dedent": "^2.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + } + } + }, "@storybook/addon-docs": { "version": "6.3.2", "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-6.3.2.tgz", diff --git a/package.json b/package.json index 9b67f738af3189..56d6ec47031de7 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "@octokit/rest": "16.26.0", "@octokit/webhooks": "7.1.0", "@storybook/addon-a11y": "6.3.2", + "@storybook/addon-controls": "6.3.12", "@storybook/addon-docs": "6.3.2", "@storybook/addon-knobs": "6.2.9", "@storybook/addon-storysource": "6.3.2", diff --git a/packages/api-fetch/src/middlewares/preloading.js b/packages/api-fetch/src/middlewares/preloading.js index a4658baefc8427..6fba40edde4951 100644 --- a/packages/api-fetch/src/middlewares/preloading.js +++ b/packages/api-fetch/src/middlewares/preloading.js @@ -52,7 +52,7 @@ function createPreloadingMiddleware( preloadedData ) { if ( 'GET' === method && cache[ path ] ) { const cacheData = cache[ path ]; - // Unsetting the cache key ensures that the data is only preloaded a single time + // Unsetting the cache key ensures that the data is only used a single time delete cache[ path ]; return Promise.resolve( @@ -72,11 +72,12 @@ function createPreloadingMiddleware( preloadedData ) { cache[ method ] && cache[ method ][ path ] ) { - return Promise.resolve( - parse - ? cache[ method ][ path ].body - : cache[ method ][ path ] - ); + const cacheData = cache[ method ][ path ]; + + // Unsetting the cache key ensures that the data is only used a single time + delete cache[ method ][ path ]; + + return Promise.resolve( parse ? cacheData.body : cacheData ); } } diff --git a/packages/api-fetch/src/middlewares/test/preloading.js b/packages/api-fetch/src/middlewares/test/preloading.js index 3f470e9a8a6e69..4add5a46424cc9 100644 --- a/packages/api-fetch/src/middlewares/test/preloading.js +++ b/packages/api-fetch/src/middlewares/test/preloading.js @@ -199,6 +199,34 @@ describe( 'Preloading Middleware', () => { expect( value ).toEqual( body ); } ); + it( 'should remove OPTIONS type requests from the cache after the first hit', async () => { + const body = { content: 'example' }; + const preloadedData = { + OPTIONS: { + 'wp/v2/demo': { body }, + }, + }; + + const preloadingMiddleware = createPreloadingMiddleware( + preloadedData + ); + + const requestOptions = { + method: 'OPTIONS', + path: 'wp/v2/demo', + }; + + const firstMiddleware = jest.fn(); + preloadingMiddleware( requestOptions, firstMiddleware ); + expect( firstMiddleware ).not.toHaveBeenCalled(); + + await preloadingMiddleware( requestOptions, firstMiddleware ); + + const secondMiddleware = jest.fn(); + await preloadingMiddleware( requestOptions, secondMiddleware ); + expect( secondMiddleware ).toHaveBeenCalledTimes( 1 ); + } ); + describe.each( [ [ 'GET' ], [ 'OPTIONS' ] ] )( '%s', ( method ) => { describe.each( [ [ 'all empty', {} ], diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index aec2e554473e90..1baae653794229 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -291,6 +291,9 @@ function InsertionPointPopover( { // Render in the old slot if needed for backward compatibility, // otherwise render in place (not in the the default popover slot). __unstableSlotName={ __unstablePopoverSlot || null } + // Forces a remount of the popover when its position changes + // This makes sure the popover doesn't animate from its previous position. + key={ nextClientId + '--' + rootClientId } > item.value === unit ); const step = unitConfig?.step || 1; diff --git a/packages/block-editor/src/components/border-radius-control/test/utils.js b/packages/block-editor/src/components/border-radius-control/test/utils.js new file mode 100644 index 00000000000000..50d932322a02f3 --- /dev/null +++ b/packages/block-editor/src/components/border-radius-control/test/utils.js @@ -0,0 +1,216 @@ +/** + * Internal dependencies + */ +import { + getAllUnit, + getAllValue, + hasMixedValues, + hasDefinedValues, + mode, +} from '../utils'; + +describe( 'getAllUnit', () => { + describe( 'when provided string based values', () => { + it( 'should return valid unit when passed a valid unit', () => { + expect( getAllUnit( '32em' ) ).toBe( 'em' ); + } ); + + it( 'should fall back to px when passed an invalid unit', () => { + expect( getAllUnit( '32apples' ) ).toBe( 'px' ); + } ); + + it( 'should fall back to px when passed a value without a unit', () => { + expect( getAllUnit( '32' ) ).toBe( 'px' ); + } ); + } ); + + describe( 'when provided object based values', () => { + it( 'should return the most common value', () => { + const values = { + bottomLeft: '2em', + bottomRight: '2em', + topLeft: '0', + topRight: '2px', + }; + expect( getAllUnit( values ) ).toBe( 'em' ); + } ); + + it( 'should return the real value when the most common value is undefined', () => { + const values = { + bottomLeft: '0', + bottomRight: '0', + topLeft: '0', + topRight: '2em', + }; + expect( getAllUnit( values ) ).toBe( 'em' ); + } ); + + it( 'should return the most common value there are no undefined values', () => { + const values = { + bottomLeft: '1em', + bottomRight: '1em', + topLeft: '2px', + topRight: '2em', + }; + expect( getAllUnit( values ) ).toBe( 'em' ); + } ); + + it( 'should fall back to px when all values are undefined or equivalent', () => { + const values = { + bottomLeft: '0', + bottomRight: undefined, + topLeft: undefined, + topRight: '0', + }; + expect( getAllUnit( values ) ).toBe( 'px' ); + } ); + } ); + + describe( 'when provided invalid values', () => { + it( 'should return px when passed an array', () => { + expect( getAllUnit( [] ) ).toBe( 'px' ); + } ); + it( 'should return px when passed a boolean', () => { + expect( getAllUnit( false ) ).toBe( 'px' ); + } ); + it( 'should return px when passed undefined', () => { + expect( getAllUnit( false ) ).toBe( 'px' ); + } ); + } ); +} ); + +describe( 'getAllValue', () => { + describe( 'when provided string based values', () => { + it( 'should return valid value + unit when passed a valid unit', () => { + expect( getAllValue( '32em' ) ).toBe( '32em' ); + } ); + + it( 'should return string as-is without parsing it', () => { + expect( getAllValue( '32apples' ) ).toBe( '32apples' ); + } ); + } ); + + describe( 'when provided object based values', () => { + it( 'should return null if values are mixed', () => { + const values = { + bottomLeft: '2em', + bottomRight: '2em', + topLeft: '0', + topRight: '2px', + }; + expect( getAllValue( values ) ).toBe( null ); + } ); + + it( 'should return the common value + unit when all values are the same', () => { + const values = { + bottomLeft: '1em', + bottomRight: '1em', + topLeft: '1em', + topRight: '1em', + }; + expect( getAllValue( values ) ).toBe( '1em' ); + } ); + + it( 'should return the common value + most common unit when same values but different units', () => { + const values = { + bottomLeft: '1em', + bottomRight: '1em', + topLeft: '1px', + topRight: '1rem', + }; + expect( getAllValue( values ) ).toBe( '1em' ); + } ); + + it( 'should fall back to null when values are undefined', () => { + const values = { + bottomLeft: undefined, + bottomRight: undefined, + topLeft: undefined, + topRight: undefined, + }; + expect( getAllValue( values ) ).toBe( null ); + } ); + } ); + + describe( 'when provided invalid values', () => { + it( 'should return px when passed an array', () => { + expect( getAllValue( [] ) ).toBe( null ); + } ); + it( 'should return px when passed a boolean', () => { + expect( getAllValue( false ) ).toBe( null ); + } ); + it( 'should return px when passed undefined', () => { + expect( getAllValue( false ) ).toBe( null ); + } ); + } ); +} ); + +describe( 'hasMixedValues', () => { + it( 'should return false when passed a string value', () => { + expect( hasMixedValues( '2px' ) ).toBe( false ); + } ); + + it( 'should return true when passed mixed values', () => { + const values = { + bottomLeft: '1em', + bottomRight: '1px', + topLeft: '2px', + topRight: '2em', + }; + expect( hasMixedValues( values ) ).toBe( true ); + } ); + + it( 'should return false when passed a common value', () => { + const values = { + bottomLeft: '1em', + bottomRight: '1em', + topLeft: '1em', + topRight: '1em', + }; + expect( hasMixedValues( values ) ).toBe( false ); + } ); +} ); + +describe( 'hasDefinedValues', () => { + it( 'should return false when passed a falsy value', () => { + expect( hasDefinedValues( undefined ) ).toBe( false ); + expect( hasDefinedValues( null ) ).toBe( false ); + expect( hasDefinedValues( '' ) ).toBe( false ); + } ); + + it( 'should return true when passed a non empty string value', () => { + expect( hasDefinedValues( '1px' ) ).toBe( true ); + } ); + + it( 'should return false when passed an object with empty values', () => { + const values = { + bottomLeft: undefined, + bottomRight: undefined, + topLeft: undefined, + topRight: undefined, + }; + expect( hasDefinedValues( values ) ).toBe( false ); + } ); + + it( 'should return true when passed an object with at least one real value', () => { + const values = { + bottomLeft: undefined, + bottomRight: '1px', + topLeft: undefined, + topRight: undefined, + }; + expect( hasDefinedValues( values ) ).toBe( true ); + } ); +} ); + +describe( 'mode', () => { + it( 'should return the most common value', () => { + const values = [ 'a', 'z', 'z', 'b', undefined ]; + expect( mode( values ) ).toBe( 'z' ); + } ); + + it( 'should return the most common real value', () => { + const values = [ undefined, 'a', undefined, undefined, undefined ]; + expect( mode( values ) ).toBe( 'a' ); + } ); +} ); diff --git a/packages/block-editor/src/components/border-radius-control/utils.js b/packages/block-editor/src/components/border-radius-control/utils.js index a5bf6e176c92db..0f628d6dce068b 100644 --- a/packages/block-editor/src/components/border-radius-control/utils.js +++ b/packages/block-editor/src/components/border-radius-control/utils.js @@ -4,27 +4,35 @@ import { __experimentalParseUnit as parseUnit } from '@wordpress/components'; /** - * Gets the item with the highest occurrence within an array - * https://stackoverflow.com/a/20762713 + * Gets the (non-undefined) item with the highest occurrence within an array + * Based in part on: https://stackoverflow.com/a/20762713 * - * @param {Array} arr Array of items to check. - * @return {any} The item with the most occurrences. + * Undefined values are always sorted to the end by `sort`, so this function + * returns the first element, to always prioritize real values over undefined + * values. + * + * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description + * + * @param {Array} inputArray Array of items to check. + * @return {any} The item with the most occurrences. */ -function mode( arr ) { +export function mode( inputArray ) { + const arr = [ ...inputArray ]; return arr .sort( ( a, b ) => - arr.filter( ( v ) => v === a ).length - - arr.filter( ( v ) => v === b ).length + inputArray.filter( ( v ) => v === b ).length - + inputArray.filter( ( v ) => v === a ).length ) - .pop(); + .shift(); } /** * Returns the most common CSS unit in the radius values. + * Falls back to `px` as a default unit. * * @param {Object|string} values Radius values. - * @return {string} Most common CSS unit in values. + * @return {string} Most common CSS unit in values. Default: `px`. */ export function getAllUnit( values = {} ) { if ( typeof values === 'string' ) { @@ -37,7 +45,7 @@ export function getAllUnit( values = {} ) { return unit; } ); - return mode( allUnits ); + return mode( allUnits ) || 'px'; } /** diff --git a/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js b/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js index 170ecab88b624e..92479384b5bcb8 100644 --- a/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js +++ b/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js @@ -10,7 +10,7 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { store as blockEditorStore } from '../../store'; import { cleanEmptyObject } from '../../hooks/utils'; -export default function BlockSupportToolsPanel( { children, label } ) { +export default function BlockSupportToolsPanel( { children, group, label } ) { const { clientId, attributes } = useSelect( ( select ) => { const { getBlockAttributes, getSelectedBlockClientId } = select( blockEditorStore @@ -46,6 +46,7 @@ export default function BlockSupportToolsPanel( { children, label } ) { return ( { setDidUploadFail( false ); - const onSelect = attributesFromMedia( setAttributes ); + const onSelect = attributesFromMedia( setAttributes, dimRatio ); onSelect( media ); }; diff --git a/packages/block-library/src/cover/shared.js b/packages/block-library/src/cover/shared.js index 5c4c20a7cb4ec6..4a45c145518f0e 100644 --- a/packages/block-library/src/cover/shared.js +++ b/packages/block-library/src/cover/shared.js @@ -32,7 +32,7 @@ export function dimRatioToClass( ratio ) { : 'has-background-dim-' + 10 * Math.round( ratio / 10 ); } -export function attributesFromMedia( setAttributes ) { +export function attributesFromMedia( setAttributes, dimRatio ) { return ( media ) => { if ( ! media || ! media.url ) { setAttributes( { url: undefined, id: undefined } ); @@ -65,6 +65,7 @@ export function attributesFromMedia( setAttributes ) { } setAttributes( { + dimRatio: dimRatio === 100 ? 50 : dimRatio, url: media.url, id: media.id, alt: media?.alt, diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index 705c2f2e8725ff..c35c20e8f514a4 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -32,6 +32,9 @@ "color": { "link": true }, + "spacing": { + "margin": true + }, "typography": { "fontSize": true, "lineHeight": true, diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index 9ca81e0410a66b..4694ccde5a46e5 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -8,12 +8,14 @@ import classnames from 'classnames'; */ import { __ } from '@wordpress/i18n'; import { useEffect } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; import { createBlock } from '@wordpress/blocks'; import { AlignmentControl, BlockControls, RichText, useBlockProps, + store as blockEditorStore, } from '@wordpress/block-editor'; /** @@ -41,15 +43,25 @@ function HeadingEdit( { style, } ); + const { __unstableMarkNextChangeAsNotPersistent } = useDispatch( + blockEditorStore + ); + // Initially set anchor for headings that have content but no anchor set. // This is used when transforming a block to heading, or for legacy anchors. useEffect( () => { if ( ! anchor && content ) { + // This side-effect should not create an undo level. + __unstableMarkNextChangeAsNotPersistent(); setAttributes( { anchor: generateAnchor( clientId, content, allHeadingAnchors ), } ); } + allHeadingAnchors[ clientId ] = anchor; + return () => { + delete allHeadingAnchors[ clientId ]; + }; }, [ content, anchor ] ); const onContentChange = ( value ) => { diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 2822b03081f024..bb447f9654c0c7 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -96,6 +96,19 @@ "__experimentalTextTransform": true, "__experimentalFontFamily": true, "__experimentalTextDecoration": true + }, + "spacing": { + "blockGap": true, + "units": [ + "px", + "em", + "rem", + "vh", + "vw" + ], + "__experimentalDefaultControls": { + "blockGap": true + } } }, "viewScript": "file:./view.min.js", diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 885ba803db15fe..5cf54dadfe3df9 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -41,12 +41,14 @@ } // Menu item link. - // By targetting the markup directly we enable greater global styles compatibility. - // The extra container specificity is due to global styles outputting link styles that need overriding. - &.wp-block-navigation a { - // Inherit colors set by the block color definition. + // By adding low specificity, we enable compatibility with link colors set in theme.json, + // but still allow them to be overridden by user-set colors. + .wp-block-navigation-item__content { color: inherit; display: block; + + // Set the default menu item padding to zero, to allow text-only buttons. + padding: 0; } // Force links to inherit text decoration applied to navigation block. @@ -229,7 +231,7 @@ .wp-block-navigation, .wp-block-navigation .wp-block-page-list, .wp-block-navigation__container { - gap: 0.5em 2em; + gap: var(--wp--style--block-gap, 2em); } // Menu items with background. @@ -240,7 +242,7 @@ &, .wp-block-navigation .wp-block-page-list, .wp-block-navigation__container { - gap: 0 0.5em; + gap: var(--wp--style--block-gap, 0.5em); } } @@ -252,14 +254,9 @@ // That way if padding is set in theme.json, it still wins. // https://css-tricks.com/almanac/selectors/w/where/ -// Set the default menu item padding to zero, to allow text-only buttons. -.wp-block-navigation .wp-block-navigation-item__content { - padding: 0; -} - // When the menu has a background, items have paddings, reduce margins to compensate. // Treat margins and paddings differently when the block has a background. -.wp-block-navigation:where(.has-background) a { +.wp-block-navigation:where(.has-background) .wp-block-navigation-item__content { padding: 0.5em 1em; } @@ -468,7 +465,7 @@ } // A default padding is added to submenu items. It's not appropriate inside the modal. - & :where(.wp-block-navigation__submenu-container) a { + a { padding: 0; } diff --git a/packages/block-library/src/post-comment-author/block.json b/packages/block-library/src/post-comment-author/block.json index c3a2231cc5d10b..7faa0b7ad9f2aa 100644 --- a/packages/block-library/src/post-comment-author/block.json +++ b/packages/block-library/src/post-comment-author/block.json @@ -6,8 +6,31 @@ "parent": [ "core/post-comment" ], "description": "Post comment author.", "textdomain": "default", + "attributes": { + "isLink": { + "type": "boolean", + "default": false + }, + "linkTarget": { + "type": "string", + "default": "_self" + } + }, "usesContext": [ "commentId" ], "supports": { - "html": false + "html": false, + "color": { + "gradients": true, + "link": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalLetterSpacing": true + } } } diff --git a/packages/block-library/src/post-comment-author/edit.js b/packages/block-library/src/post-comment-author/edit.js index d63d1cf5a958fa..ed4a3f4733ae5f 100644 --- a/packages/block-library/src/post-comment-author/edit.js +++ b/packages/block-library/src/post-comment-author/edit.js @@ -3,12 +3,24 @@ */ import { __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; -import { useBlockProps } from '@wordpress/block-editor'; +import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; +import { PanelBody, ToggleControl } from '@wordpress/components'; -export default function Edit( { attributes, context } ) { - const { className } = attributes; +/** + * Renders the `core/post-comment-author` block on the editor. + * + * @param {Object} props React props. + * @param {Object} props.setAttributes Callback for updating block attributes. + * @param {Object} props.attributes Block attributes. + * @param {Object} props.context Inherited context. + * + * @return {JSX.Element} React element. + */ +export default function Edit( { attributes, context, setAttributes } ) { + const { className, isLink, linkTarget } = attributes; const { commentId } = context; + const blockProps = useBlockProps( { className } ); const displayName = useSelect( ( select ) => { @@ -21,15 +33,45 @@ export default function Edit( { attributes, context } ) { const user = getEntityRecord( 'root', 'user', comment.author ); return user?.name ?? __( 'Anonymous' ); } - return authorName ?? ''; }, [ commentId ] ); + const displayAuthor = isLink ? ( + event.preventDefault() } + > + { displayName } + + ) : ( +

{ displayName }

+ ); + return ( -
-

{ displayName }

-
+ <> + + + setAttributes( { isLink: ! isLink } ) } + checked={ isLink } + /> + { isLink && ( + + setAttributes( { + linkTarget: value ? '_blank' : '_self', + } ) + } + checked={ linkTarget === '_blank' } + /> + ) } + + +
{ displayAuthor }
+ ); } diff --git a/packages/block-library/src/post-comment-author/index.js b/packages/block-library/src/post-comment-author/index.js index 68fb2c2f737c25..e6779fa3ec22a8 100644 --- a/packages/block-library/src/post-comment-author/index.js +++ b/packages/block-library/src/post-comment-author/index.js @@ -7,7 +7,7 @@ import edit from './edit'; /** * WordPress dependencies */ -import { postAuthor as icon } from '@wordpress/icons'; +import { commentAuthor as icon } from '@wordpress/icons'; const { name } = metadata; export { metadata, name }; diff --git a/packages/block-library/src/post-comment-author/index.php b/packages/block-library/src/post-comment-author/index.php index 9cad587d3d447c..430b0c8d4b254f 100644 --- a/packages/block-library/src/post-comment-author/index.php +++ b/packages/block-library/src/post-comment-author/index.php @@ -11,7 +11,7 @@ * @param array $attributes Block attributes. * @param string $content Block default content. * @param WP_Block $block Block instance. - * @return string Return the post comment's content. + * @return string Return the post comment's author. */ function render_block_core_post_comment_author( $attributes, $content, $block ) { if ( ! isset( $block->context['commentId'] ) ) { @@ -19,11 +19,17 @@ function render_block_core_post_comment_author( $attributes, $content, $block ) } $wrapper_attributes = get_block_wrapper_attributes(); + $comment_author = get_comment_author( $block->context['commentId'] ); + $link = get_comment_author_url( $block->context['commentId'] ); + + if ( ! empty( $attributes['isLink'] ) && ! empty( $attributes['linkTarget'] ) ) { + $comment_author = sprintf( '%3s', $link, $attributes['linkTarget'], $comment_author ); + } return sprintf( '
%2$s
', $wrapper_attributes, - get_comment_author( $block->context['commentId'] ) + $comment_author ); } diff --git a/packages/block-library/src/post-featured-image/block.json b/packages/block-library/src/post-featured-image/block.json index 4ca50131925d8f..49116ef908199a 100644 --- a/packages/block-library/src/post-featured-image/block.json +++ b/packages/block-library/src/post-featured-image/block.json @@ -29,7 +29,11 @@ "text": false, "background": false }, - "html": false + "html": false, + "spacing": { + "margin": true, + "padding": true + } }, "editorStyle": "wp-block-post-featured-image-editor", "style": "wp-block-post-featured-image" diff --git a/packages/block-library/src/post-title/block.json b/packages/block-library/src/post-title/block.json index 308eced2ce359f..c9bfc1ce38f300 100644 --- a/packages/block-library/src/post-title/block.json +++ b/packages/block-library/src/post-title/block.json @@ -35,6 +35,9 @@ "gradients": true, "link": true }, + "spacing": { + "margin": true + }, "typography": { "fontSize": true, "lineHeight": true, diff --git a/packages/block-library/src/query-title/block.json b/packages/block-library/src/query-title/block.json index 3e5d43fc50846d..352280417c9909 100644 --- a/packages/block-library/src/query-title/block.json +++ b/packages/block-library/src/query-title/block.json @@ -23,6 +23,9 @@ "color": { "gradients": true }, + "spacing": { + "margin": true + }, "typography": { "fontSize": true, "lineHeight": true, diff --git a/packages/block-library/src/search/style.scss b/packages/block-library/src/search/style.scss index f857085aa972df..da8ca3e866aa83 100644 --- a/packages/block-library/src/search/style.scss +++ b/packages/block-library/src/search/style.scss @@ -1,69 +1,71 @@ -.wp-block-search { +.wp-block-search__button { + background: #f7f7f7; + border: 1px solid #ccc; + padding: 0.375em 0.625em; + color: #32373c; + margin-left: 0.625em; + word-break: normal; + font-size: inherit; + font-family: inherit; + line-height: inherit; - .wp-block-search__button { - background: #f7f7f7; - border: 1px solid #ccc; - padding: 0.375em 0.625em; - color: #32373c; - margin-left: 0.625em; - word-break: normal; - - &.has-icon { - line-height: 0; - } - - svg { - min-width: 1.5em; - min-height: 1.5em; - fill: currentColor; - } + &.has-icon { + line-height: 0; } - .wp-block-search__inside-wrapper { - display: flex; - flex: auto; - flex-wrap: nowrap; - max-width: 100%; + svg { + min-width: 1.5em; + min-height: 1.5em; + fill: currentColor; } +} - .wp-block-search__label { - width: 100%; - } +.wp-block-search__inside-wrapper { + display: flex; + flex: auto; + flex-wrap: nowrap; + max-width: 100%; +} - .wp-block-search__input { - padding: 8px; - flex-grow: 1; - min-width: 3em; - border: 1px solid #949494; - } +.wp-block-search__label { + width: 100%; +} - &.wp-block-search__button-only { - .wp-block-search__button { - margin-left: 0; - } - } +.wp-block-search__input { + padding: 8px; + flex-grow: 1; + min-width: 3em; + border: 1px solid #949494; + font-size: inherit; + font-family: inherit; + line-height: inherit; +} - &.wp-block-search__button-inside .wp-block-search__inside-wrapper { - padding: 4px; - border: 1px solid #949494; +.wp-block-search.wp-block-search__button-only { + .wp-block-search__button { + margin-left: 0; + } +} - .wp-block-search__input { - border-radius: 0; - border: none; - padding: 0 0 0 0.25em; +.wp-block-search.wp-block-search__button-inside .wp-block-search__inside-wrapper { + padding: 4px; + border: 1px solid #949494; - &:focus { - outline: none; - } - } + .wp-block-search__input { + border-radius: 0; + border: none; + padding: 0 0 0 0.25em; - .wp-block-search__button { - padding: 0.125em 0.5em; + &:focus { + outline: none; } } - &.aligncenter .wp-block-search__inside-wrapper { - margin: auto; + .wp-block-search__button { + padding: 0.125em 0.5em; } } +.wp-block-search.aligncenter .wp-block-search__inside-wrapper { + margin: auto; +} diff --git a/packages/block-library/src/template-part/edit/placeholder/index.js b/packages/block-library/src/template-part/edit/placeholder/index.js index a32d425129ed60..cb405fb283f127 100644 --- a/packages/block-library/src/template-part/edit/placeholder/index.js +++ b/packages/block-library/src/template-part/edit/placeholder/index.js @@ -95,12 +95,14 @@ export default function TemplatePartPlaceholder( { enableSelection ? sprintf( // Translators: %s as template part area title ("Header", "Footer", etc.). - 'Choose an existing %s or create a new one.', + __( + 'Choose an existing %s or create a new one.' + ), areaLabel.toLowerCase() ) : sprintf( // Translators: %s as template part area title ("Header", "Footer", etc.). - 'Create a new %s.', + __( 'Create a new %s.' ), areaLabel.toLowerCase() ) } @@ -136,7 +138,7 @@ export default function TemplatePartPlaceholder( { > { sprintf( // Translators: %s as template part area title ("Header", "Footer", etc.). - 'New %s', + __( 'New %s' ), areaLabel.toLowerCase() ) } diff --git a/packages/block-library/src/template-part/edit/placeholder/patterns-setup.js b/packages/block-library/src/template-part/edit/placeholder/patterns-setup.js index fc4f95e89756b8..bd28c4679fb313 100644 --- a/packages/block-library/src/template-part/edit/placeholder/patterns-setup.js +++ b/packages/block-library/src/template-part/edit/placeholder/patterns-setup.js @@ -62,7 +62,8 @@ export default function PatternsSetup( { { isTitleStep && ( diff --git a/packages/block-library/src/template-part/edit/selection/template-part-previews.js b/packages/block-library/src/template-part/edit/selection/template-part-previews.js index 83d6f94341930f..612629358d8b80 100644 --- a/packages/block-library/src/template-part/edit/selection/template-part-previews.js +++ b/packages/block-library/src/template-part/edit/selection/template-part-previews.js @@ -149,7 +149,9 @@ function TemplatePartsByArea( { > { sprintf( // Translators: %s for the template part variation ("Header", "Footer", "Template Part"). - 'There is no other %s available. If you are looking for another type of template part, try searching for it using the input above.', + __( + 'There is no other %s available. If you are looking for another type of template part, try searching for it using the input above.' + ), area && area !== 'uncategorized' ? labelsByArea[ area ] || area : __( 'Template Part' ) diff --git a/packages/block-library/src/template-part/variations.js b/packages/block-library/src/template-part/variations.js index da41d03f22339d..ca862f5993ccc6 100644 --- a/packages/block-library/src/template-part/variations.js +++ b/packages/block-library/src/template-part/variations.js @@ -7,7 +7,7 @@ import { header as headerIcon, footer as footerIcon, sidebar as sidebarIcon, - layout as layoutIcon, + symbolFilled as symbolFilledIcon, } from '@wordpress/icons'; /** @@ -23,7 +23,7 @@ function getTemplatePartIcon( iconName ) { } else if ( 'sidebar' === iconName ) { return sidebarIcon; } - return layoutIcon; + return symbolFilledIcon; } export function enhanceTemplatePartVariations( settings, name ) { diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index 088f56f43d6bb4..f5cda274bb7610 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -277,14 +277,24 @@ import Button from '../'; export default { title: 'Components/Button', component: Button }; -export const _default = () => ; - -export const primary = () => ; - -export const secondary = () => ; +const Template = ( args ) =>