From b88fa5f0825112434fef52ff108c1190be018162 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 29 Mar 2023 17:18:14 +0000 Subject: [PATCH] Merge changes published in the Gutenberg plugin "release/15.5" branch --- .github/ISSUE_TEMPLATE/New_release.md | 52 ++ .github/setup-node/action.yml | 46 ++ .github/workflows/build-plugin-zip.yml | 14 +- .github/workflows/bundle-size.yml | 4 +- .github/workflows/create-block.yml | 15 +- .github/workflows/end2end-test.yml | 45 +- .../workflows/gradle-wrapper-validation.yml | 2 +- .github/workflows/performance.yml | 40 +- .github/workflows/publish-npm-packages.yml | 6 +- .github/workflows/pull-request-automation.yml | 4 +- .github/workflows/rnmobile-android-runner.yml | 15 +- .github/workflows/rnmobile-ios-runner.yml | 11 +- .github/workflows/static-checks.yml | 4 +- .github/workflows/storybook-pages.yml | 12 +- .github/workflows/unit-test.yml | 45 +- .../upload-release-to-plugin-repo.yml | 2 +- bin/log-performance-results.js | 11 +- bin/plugin/commands/performance.js | 131 ++-- bin/plugin/lib/utils.js | 1 + bin/plugin/utils/.wp-env.performance.json | 13 - changelog.txt | 323 +++++++- docs/README.md | 26 +- docs/explanations/architecture/README.md | 20 +- .../full-site-editing-templates.md | 2 +- .../explanations/architecture/key-concepts.md | 26 +- docs/explanations/architecture/styles.md | 63 +- .../data-basics/2-building-a-list-of-pages.md | 2 +- .../javascript/versions-and-building.md | 3 +- docs/how-to-guides/themes/theme-json.md | 2 +- docs/manifest.json | 2 +- .../block-api/block-metadata.md | 36 + .../block-api/block-selectors.md | 116 +++ docs/reference-guides/core-blocks.md | 10 +- .../theme-json-reference/theme-json-living.md | 1 + gutenberg.php | 2 +- lib/block-supports/duotone.php | 129 +--- lib/block-supports/elements.php | 19 +- lib/block-supports/layout.php | 33 +- lib/class-wp-duotone-gutenberg.php | 389 ++++++++++ lib/class-wp-theme-json-gutenberg.php | 324 ++++---- lib/compat/wordpress-6.1/blocks.php | 2 + .../wordpress-6.1/template-parts-screen.php | 2 +- lib/compat/wordpress-6.2/block-patterns.php | 2 +- lib/compat/wordpress-6.3/blocks.php | 28 + .../get-global-styles-and-settings.php | 155 ++++ lib/compat/wordpress-6.3/rest-api.php | 2 +- lib/compat/wordpress-6.3/script-loader.php | 15 + lib/experimental/editor-settings.php | 3 + .../class-wp-fonts-provider-local.php | 7 +- lib/experimental/kses.php | 19 + lib/experiments-page.php | 12 + lib/load.php | 6 + lib/theme.json | 25 + package-lock.json | 40 +- package.json | 8 +- packages/api-fetch/README.md | 20 +- packages/babel-plugin-makepot/index.js | 2 +- packages/babel-preset-default/CHANGELOG.md | 4 + packages/babel-preset-default/bin/index.js | 15 +- packages/base-styles/_z-index.scss | 5 + packages/block-editor/CHANGELOG.md | 2 + packages/block-editor/README.md | 4 + .../components/block-draggable/content.scss | 2 +- .../src/components/block-list/block-html.js | 2 +- .../src/components/block-preview/auto.js | 19 +- .../components/colors-gradients/style.scss | 16 +- .../src/components/editor-styles/index.js | 30 +- .../components/global-styles/color-panel.js | 706 ++++++++++++++++++ .../global-styles/dimensions-panel.js | 55 +- .../global-styles/get-block-css-selector.js | 129 ++++ .../src/components/global-styles/hooks.js | 80 ++ .../src/components/global-styles/index.js | 3 +- .../test/use-global-styles-output.js | 31 +- .../global-styles/typography-panel.js | 77 +- .../global-styles/use-global-styles-output.js | 243 ++++-- .../src/components/global-styles/utils.js | 3 + .../components/image-size-control/index.js | 7 +- .../image-size-control/test/index.js | 4 +- .../src/components/inner-blocks/README.md | 2 +- .../position-controls-panel.js | 49 +- .../components/line-height-control/index.js | 11 +- .../src/components/list-view/appender.js | 101 +++ .../src/components/list-view/block.js | 9 +- .../src/components/list-view/branch.js | 31 +- .../src/components/list-view/index.js | 53 +- .../src/components/list-view/style.scss | 19 + .../components/media-replace-flow/index.js | 60 +- .../components/media-replace-flow/style.scss | 7 +- .../src/components/rich-text/format-edit.js | 34 +- .../src/components/writing-flow/use-input.js | 9 +- packages/block-editor/src/hooks/border.js | 1 - packages/block-editor/src/hooks/color.js | 416 +++-------- .../block-editor/src/hooks/content-lock-ui.js | 8 +- .../{color-panel.js => contrast-checker.js} | 56 +- packages/block-editor/src/hooks/dimensions.js | 1 - packages/block-editor/src/hooks/duotone.js | 13 +- packages/block-editor/src/hooks/position.js | 6 +- packages/block-editor/src/hooks/style.js | 57 +- packages/block-editor/src/hooks/test/utils.js | 104 --- packages/block-editor/src/hooks/typography.js | 1 - packages/block-editor/src/hooks/utils.js | 97 +-- packages/block-editor/src/layouts/grid.js | 172 +++++ packages/block-editor/src/layouts/index.js | 3 +- .../block-editor/src/layouts/test/grid.js | 21 + packages/block-editor/src/private-apis.js | 2 + packages/block-editor/src/utils/object.js | 69 ++ .../block-editor/src/utils/test/object.js | 107 +++ packages/block-library/src/buttons/block.json | 1 + .../block-library/src/categories/editor.scss | 5 + .../block-library/src/categories/style.scss | 4 + packages/block-library/src/columns/block.json | 4 + packages/block-library/src/columns/edit.js | 3 +- packages/block-library/src/comments/index.php | 1 + .../src/cover/edit/inspector-controls.js | 9 +- .../__snapshots__/transforms.native.js.snap | 4 +- .../src/cover/test/block-controls.js | 62 -- packages/block-library/src/cover/test/edit.js | 324 ++++++++ packages/block-library/src/gallery/edit.js | 7 +- .../src/gallery/use-image-sizes.js | 2 +- .../block-library/src/group/deprecated.js | 6 +- packages/block-library/src/group/edit.js | 36 +- .../block-library/src/group/placeholder.js | 14 +- .../block-library/src/group/variations.js | 15 +- .../block-library/src/home-link/index.php | 4 +- packages/block-library/src/image/block.json | 9 +- packages/block-library/src/image/edit.js | 2 +- .../block-library/src/image/edit.native.js | 2 +- packages/block-library/src/image/image.js | 22 +- packages/block-library/src/image/index.php | 9 +- .../__snapshots__/transforms.native.js.snap | 2 +- .../src/latest-comments/style.scss | 2 +- .../block-library/src/latest-posts/edit.js | 3 + .../block-library/src/media-text/block.json | 2 +- .../src/media-text/deprecated.js | 238 +++++- packages/block-library/src/media-text/edit.js | 13 +- .../__snapshots__/transforms.native.js.snap | 8 +- .../src/navigation-link/index.php | 3 +- .../src/navigation-submenu/index.php | 106 +-- .../block-library/src/navigation/block.json | 1 - .../src/navigation/edit/index.js | 13 - .../src/navigation/edit/inner-blocks.js | 3 - .../block-library/src/navigation/style.scss | 10 +- .../block-library/src/post-author/index.php | 2 +- packages/block-library/src/post-date/edit.js | 5 +- packages/block-library/src/post-date/index.js | 2 + .../block-library/src/post-date/variations.js | 20 + .../block-library/src/post-excerpt/edit.js | 64 +- .../post-featured-image/dimension-controls.js | 4 +- .../src/post-featured-image/editor.scss | 1 + .../src/post-template/block.json | 2 +- .../src/post-template/style.scss | 2 +- .../src/post-time-to-read/block.json | 18 +- .../src/post-time-to-read/style.scss | 4 + .../block-library/src/query-title/index.php | 7 +- packages/block-library/src/quote/block.json | 1 + packages/block-library/src/search/style.scss | 28 +- packages/block-library/src/spacer/edit.js | 23 +- packages/block-library/src/style.scss | 1 + .../__snapshots__/transforms.native.js.snap | 2 +- packages/blocks/src/api/constants.js | 4 + packages/blocks/src/api/registration.js | 13 + packages/blocks/src/api/test/registration.js | 57 ++ .../blocks/src/store/private-selectors.js | 2 + .../src/store/test/private-selectors.js | 6 + packages/components/CHANGELOG.md | 34 +- .../src/angle-picker-control/angle-circle.tsx | 1 - .../src/angle-picker-control/index.tsx | 41 +- .../styles/angle-picker-control-styles.tsx | 25 +- packages/components/src/animate/index.js | 60 -- packages/components/src/animate/index.tsx | 75 ++ .../components/src/animate/stories/index.js | 53 -- .../components/src/animate/stories/index.tsx | 102 +++ packages/components/src/animate/types.ts | 32 + .../src/border-box-control/stories/index.tsx | 2 +- .../src/box-control/axial-input-controls.tsx | 2 +- .../src/circular-option-picker/style.scss | 10 + .../components/src/color-indicator/style.scss | 1 + .../components/src/color-picker/README.md | 4 +- .../src/color-picker/color-input.tsx | 13 +- .../components/src/color-picker/component.tsx | 27 +- .../components/src/color-picker/hex-input.tsx | 9 +- .../components/src/color-picker/hsl-input.tsx | 9 +- .../src/color-picker/input-with-slider.tsx | 10 +- .../src/color-picker/legacy-adapter.tsx | 3 +- .../components/src/color-picker/picker.tsx | 11 +- .../components/src/color-picker/rgb-input.tsx | 9 +- .../src/color-picker/stories/index.js | 41 - .../src/color-picker/stories/index.tsx | 51 ++ .../color-picker/test/{index.js => index.tsx} | 54 +- packages/components/src/color-picker/types.ts | 129 +++- .../src/color-picker/use-deprecated-props.ts | 55 +- .../src/combobox-control/stories/index.tsx | 2 +- .../{constants.js => constants.ts} | 4 +- .../{constants.js => constants.ts} | 0 .../{control-points.js => control-points.tsx} | 305 ++++---- .../gradient-bar/{index.js => index.tsx} | 42 +- .../gradient-bar/test/{utils.js => utils.ts} | 89 ++- .../gradient-bar/utils.js | 189 ----- .../gradient-bar/utils.ts | 193 +++++ .../{index.js => index.tsx} | 85 ++- .../src/custom-gradient-picker/serializer.js | 48 -- .../src/custom-gradient-picker/serializer.ts | 82 ++ .../custom-gradient-picker/stories/index.js | 33 - .../custom-gradient-picker/stories/index.tsx | 45 ++ .../src/custom-gradient-picker/style.scss | 29 +- ...s.js => custom-gradient-picker-styles.tsx} | 0 .../test/{serializer.js => serializer.ts} | 72 +- .../src/custom-gradient-picker/types.ts | 137 ++++ .../{utils.js => utils.ts} | 59 +- .../src/custom-select-control/test/index.js | 10 +- .../color-list-picker/index.tsx | 13 +- .../color-list-picker/style.scss | 0 .../color-list-picker/types.ts | 4 +- ...-duotone-bar.js => custom-duotone-bar.tsx} | 8 +- .../{duotone-picker.js => duotone-picker.tsx} | 41 +- .../{duotone-swatch.js => duotone-swatch.tsx} | 3 +- .../src/duotone-picker/{index.js => index.ts} | 0 .../{duotone-picker.js => duotone-picker.tsx} | 23 +- .../{duotone-swatch.js => duotone-swatch.tsx} | 12 +- .../components/src/duotone-picker/types.ts | 61 ++ .../src/duotone-picker/{utils.js => utils.ts} | 43 +- .../components/src/flex/flex-item/README.md | 2 +- .../components/src/form-token-field/README.md | 1 + .../components/src/form-token-field/index.tsx | 13 +- .../src/form-token-field/style.scss | 7 - .../src/form-token-field/test/index.tsx | 1 + .../components/src/form-token-field/types.ts | 6 + .../components/src/gradient-picker/README.md | 39 +- .../gradient-picker/{index.js => index.tsx} | 86 ++- .../stories/{index.js => index.tsx} | 27 +- .../components/src/gradient-picker/types.ts | 89 +++ .../components/src/item-group/test/index.js | 8 +- .../components/src/palette-edit/index.tsx | 9 +- .../src/resizable-box/resize-tooltip/utils.ts | 2 +- .../components/src/sandbox/index.native.js | 149 ++-- .../components/src/slot-fill/test/slot.js | 14 +- packages/components/src/style.scss | 2 +- packages/components/src/tab-panel/index.tsx | 9 +- .../components/src/tools-panel/test/index.tsx | 86 +++ .../src/tools-panel/tools-panel/hook.ts | 48 +- packages/components/src/tools-panel/types.ts | 1 + packages/components/src/tooltip/test/index.js | 552 +++++++------- .../src/utils/input/input-control.js | 1 + packages/components/tsconfig.json | 3 +- .../src/hooks/use-media-query/index.js | 65 +- .../src/hooks/use-media-query/test/index.js | 110 +-- packages/data/CHANGELOG.md | 4 + .../data/src/components/use-select/index.js | 144 ++-- .../src/components/use-select/test/index.js | 410 ++++++---- packages/data/src/registry.js | 28 +- packages/data/src/test/registry.js | 53 +- .../src/request-utils/pages.ts | 4 +- .../e2e-test-utils-playwright/src/test.ts | 5 +- .../e2e-tests/config/performance-reporter.js | 6 +- .../specs/editor/blocks/cover.test.js | 45 -- .../performance/front-end-block-theme.test.js | 15 +- .../front-end-classic-theme.test.js | 15 +- .../specs/performance/post-editor.test.js | 50 +- .../specs/performance/site-editor.test.js | 22 +- packages/e2e-tests/specs/performance/utils.js | 18 +- packages/edit-navigation/src/private-apis.js | 10 - .../components/secondary-sidebar/style.scss | 6 + .../src/components/visual-editor/index.js | 2 +- packages/edit-post/src/store/actions.js | 2 +- packages/edit-post/src/style.scss | 10 + .../add-new-template/new-template-part.js | 7 +- .../add-new-template/new-template.js | 9 +- .../src/components/add-new-template/utils.js | 2 +- .../components/block-editor/editor-canvas.js | 36 +- .../global-styles-renderer/index.js | 3 +- .../components/global-styles/color-utils.js | 14 - .../components/global-styles/context-menu.js | 4 +- .../src/components/global-styles/hooks.js | 79 +- .../src/components/global-styles/preview.js | 2 +- .../global-styles/screen-background-color.js | 132 ---- .../global-styles/screen-block-list.js | 4 +- .../global-styles/screen-button-color.js | 104 --- .../components/global-styles/screen-colors.js | 254 +------ .../global-styles/screen-heading-color.js | 206 ----- .../global-styles/screen-link-color.js | 124 --- .../global-styles/screen-text-color.js | 62 -- .../screen-typography-element.js | 4 + .../global-styles/screen-typography.js | 6 + .../components/global-styles/stories/index.js | 425 +++++++++++ .../src/components/global-styles/style.scss | 22 +- .../src/components/global-styles/ui.js | 37 +- .../secondary-sidebar/list-view-sidebar.js | 7 +- .../start-template-options/index.js | 48 +- .../src/components/style-book/index.js | 26 +- .../src/components/style-book/style.scss | 2 +- .../push-changes-to-global-styles/index.js | 1 + .../src/components/post-locked-modal/index.js | 116 ++- .../components/post-locked-modal/style.scss | 6 +- .../post-publish-panel/postpublish.js | 5 + .../post-type-support-check/index.js | 2 +- .../components/theme-support-check/index.js | 2 +- packages/editor/src/utils/terms.js | 24 +- packages/element/README.md | 72 +- .../element/src/create-interpolate-element.js | 6 +- packages/element/src/react-platform.js | 17 +- packages/env/README.md | 3 +- packages/env/lib/config/config.js | 2 +- packages/env/lib/config/test/config.js | 6 +- packages/icons/src/index.js | 1 + packages/icons/src/library/unseen.js | 12 + packages/plugins/README.md | 6 + packages/plugins/package.json | 1 + .../src/components/plugin-area/index.js | 125 ++-- .../src/components/test/plugin-area.js | 117 +++ packages/private-apis/tsconfig.json | 3 - .../react-native-aztec/android/build.gradle | 2 +- .../ios/RNTAztecView/RCTAztecView.swift | 2 +- packages/react-native-aztec/package.json | 2 +- .../android/react-native-bridge/build.gradle | 2 +- .../ReactNativeGutenbergBridge/RNMedia.kt | 1 + .../wordpress/mobile/WPAndroidGlue/Media.kt | 8 +- .../ios/GutenbergBridgeDelegate.swift | 8 +- ...utenbergWebSingleBlockViewController.swift | 2 +- .../ios/RNReactNativeGutenbergBridge.swift | 3 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 3 + .../__device-tests__/README.md | 16 +- .../__device-tests__/helpers/utils.js | 21 + .../__device-tests__/pages/editor-page.js | 37 +- .../android/app/build.gradle | 4 +- .../java/com/gutenberg/MainApplication.java | 20 +- .../GutenbergViewController.swift | 3 +- packages/react-native-editor/ios/Podfile.lock | 8 +- packages/react-native-editor/package.json | 2 +- packages/rich-text/src/get-active-format.js | 2 +- packages/rich-text/src/get-active-formats.js | 57 +- .../src/test/__snapshots__/to-dom.js.snap | 4 +- .../rich-text/src/test/get-active-format.js | 15 +- packages/rich-text/src/test/toggle-format.js | 9 +- packages/scripts/CHANGELOG.md | 4 + .../jest-environment-puppeteer/index.js | 12 +- packages/scripts/package.json | 2 +- packages/scripts/scripts/test-e2e.js | 9 + packages/style-engine/src/index.ts | 17 +- phpunit/block-supports/duotone-test.php | 40 - .../render-block-navigation-submenu-test.php | 213 ++++++ phpunit/bootstrap.php | 41 +- phpunit/class-wp-duotone-test.php | 92 +++ .../class-wp-get-block-css-selectors-test.php | 331 ++++++++ phpunit/class-wp-theme-json-test.php | 64 +- .../wpFonts/registerProvider-test.php | 2 +- .../wpFonts/removeVariation-test.php | 2 +- readme.txt | 6 + schemas/json/block.json | 98 +++ schemas/json/theme.json | 22 +- storybook/decorators/with-rtl.js | 22 +- storybook/main.js | 1 + .../package-styles/block-editor-ltr.lazy.scss | 1 + .../package-styles/block-editor-rtl.lazy.scss | 1 + .../block-library-ltr.lazy.scss | 3 + .../block-library-rtl.lazy.scss | 3 + .../package-styles/components-ltr.lazy.scss | 1 + .../package-styles/components-rtl.lazy.scss | 1 + storybook/package-styles/config.js | 56 ++ .../package-styles/edit-site-ltr.lazy.scss | 1 + .../package-styles/edit-site-rtl.lazy.scss | 1 + .../format-library-ltr.lazy.scss | 1 + .../format-library-rtl.lazy.scss | 1 + storybook/style-ltr.lazy.scss | 7 - storybook/style-rtl.lazy.scss | 7 - .../specs/editor/blocks/navigation.spec.js | 464 +++++++----- test/e2e/specs/editor/blocks/quote.spec.js | 18 + .../specs/editor/plugins/image-size.spec.js | 2 +- .../editor/various/splitting-merging.spec.js | 91 ++- .../editor/various/switch-to-draft.spec.js | 111 ++- .../site-editor-url-navigation.spec.js | 78 ++ .../site-editor/style-variations.spec.js | 8 +- .../specs/site-editor/template-part.spec.js | 48 ++ test/e2e/specs/site-editor/title.spec.js | 9 +- .../fixtures/blocks/core__media-text.html | 2 +- .../fixtures/blocks/core__media-text.json | 2 +- .../blocks/core__media-text.parsed.json | 4 +- .../blocks/core__media-text.serialized.html | 2 +- ..._media-text__deprecated-v5.serialized.html | 2 +- ...ext__deprecated-v6-wide-default-align.html | 12 + ...ext__deprecated-v6-wide-default-align.json | 28 + ...precated-v6-wide-default-align.parsed.json | 28 + ...ated-v6-wide-default-align.serialized.html | 5 + ...a-text__image-alt-no-align.serialized.html | 2 +- ...t__image-fill-no-focal-point-selected.json | 2 +- ...ll-no-focal-point-selected.serialized.html | 2 +- ..._image-fill-with-focal-point-selected.json | 2 +- ...-with-focal-point-selected.serialized.html | 2 +- ...ore__media-text__is-stacked-on-mobile.json | 4 +- ...text__is-stacked-on-mobile.serialized.html | 2 +- ...re__media-text__vertical-align-bottom.json | 2 +- ...ext__vertical-align-bottom.serialized.html | 2 +- .../blocks/core__media-text__video.json | 4 +- .../core__media-text__video.serialized.html | 2 +- .../helpers/integration-test-editor.js | 125 ++++ .../helpers/wait-for-store-resolvers.js | 40 + test/integration/helpers/with-fake-timers.js | 30 + 397 files changed, 10522 insertions(+), 5250 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/New_release.md create mode 100644 .github/setup-node/action.yml delete mode 100644 bin/plugin/utils/.wp-env.performance.json create mode 100644 docs/reference-guides/block-api/block-selectors.md create mode 100644 lib/class-wp-duotone-gutenberg.php create mode 100644 lib/compat/wordpress-6.3/blocks.php create mode 100644 lib/compat/wordpress-6.3/get-global-styles-and-settings.php create mode 100644 lib/compat/wordpress-6.3/script-loader.php create mode 100644 packages/block-editor/src/components/global-styles/color-panel.js create mode 100644 packages/block-editor/src/components/global-styles/get-block-css-selector.js create mode 100644 packages/block-editor/src/components/list-view/appender.js rename packages/block-editor/src/hooks/{color-panel.js => contrast-checker.js} (52%) create mode 100644 packages/block-editor/src/layouts/grid.js create mode 100644 packages/block-editor/src/layouts/test/grid.js create mode 100644 packages/block-editor/src/utils/object.js create mode 100644 packages/block-editor/src/utils/test/object.js delete mode 100644 packages/block-library/src/cover/test/block-controls.js create mode 100644 packages/block-library/src/cover/test/edit.js create mode 100644 packages/block-library/src/post-date/variations.js create mode 100644 packages/block-library/src/post-time-to-read/style.scss delete mode 100644 packages/components/src/animate/index.js create mode 100644 packages/components/src/animate/index.tsx delete mode 100644 packages/components/src/animate/stories/index.js create mode 100644 packages/components/src/animate/stories/index.tsx create mode 100644 packages/components/src/animate/types.ts delete mode 100644 packages/components/src/color-picker/stories/index.js create mode 100644 packages/components/src/color-picker/stories/index.tsx rename packages/components/src/color-picker/test/{index.js => index.tsx} (76%) rename packages/components/src/custom-gradient-picker/{constants.js => constants.ts} (96%) rename packages/components/src/custom-gradient-picker/gradient-bar/{constants.js => constants.ts} (100%) rename packages/components/src/custom-gradient-picker/gradient-bar/{control-points.js => control-points.tsx} (56%) rename packages/components/src/custom-gradient-picker/gradient-bar/{index.js => index.tsx} (82%) rename packages/components/src/custom-gradient-picker/gradient-bar/test/{utils.js => utils.ts} (50%) delete mode 100644 packages/components/src/custom-gradient-picker/gradient-bar/utils.js create mode 100644 packages/components/src/custom-gradient-picker/gradient-bar/utils.ts rename packages/components/src/custom-gradient-picker/{index.js => index.tsx} (69%) delete mode 100644 packages/components/src/custom-gradient-picker/serializer.js create mode 100644 packages/components/src/custom-gradient-picker/serializer.ts delete mode 100644 packages/components/src/custom-gradient-picker/stories/index.js create mode 100644 packages/components/src/custom-gradient-picker/stories/index.tsx rename packages/components/src/custom-gradient-picker/styles/{custom-gradient-picker-styles.js => custom-gradient-picker-styles.tsx} (100%) rename packages/components/src/custom-gradient-picker/test/{serializer.js => serializer.ts} (56%) create mode 100644 packages/components/src/custom-gradient-picker/types.ts rename packages/components/src/custom-gradient-picker/{utils.js => utils.ts} (60%) rename packages/components/src/{ => duotone-picker}/color-list-picker/index.tsx (86%) rename packages/components/src/{ => duotone-picker}/color-list-picker/style.scss (100%) rename packages/components/src/{ => duotone-picker}/color-list-picker/types.ts (91%) rename packages/components/src/duotone-picker/{custom-duotone-bar.js => custom-duotone-bar.tsx} (84%) rename packages/components/src/duotone-picker/{duotone-picker.js => duotone-picker.tsx} (70%) rename packages/components/src/duotone-picker/{duotone-swatch.js => duotone-swatch.tsx} (79%) rename packages/components/src/duotone-picker/{index.js => index.ts} (100%) rename packages/components/src/duotone-picker/stories/{duotone-picker.js => duotone-picker.tsx} (69%) rename packages/components/src/duotone-picker/stories/{duotone-swatch.js => duotone-swatch.tsx} (63%) create mode 100644 packages/components/src/duotone-picker/types.ts rename packages/components/src/duotone-picker/{utils.js => utils.ts} (63%) rename packages/components/src/gradient-picker/{index.js => index.tsx} (70%) rename packages/components/src/gradient-picker/stories/{index.js => index.tsx} (78%) create mode 100644 packages/components/src/gradient-picker/types.ts delete mode 100644 packages/edit-navigation/src/private-apis.js delete mode 100644 packages/edit-site/src/components/global-styles/color-utils.js delete mode 100644 packages/edit-site/src/components/global-styles/screen-background-color.js delete mode 100644 packages/edit-site/src/components/global-styles/screen-button-color.js delete mode 100644 packages/edit-site/src/components/global-styles/screen-heading-color.js delete mode 100644 packages/edit-site/src/components/global-styles/screen-link-color.js delete mode 100644 packages/edit-site/src/components/global-styles/screen-text-color.js create mode 100644 packages/edit-site/src/components/global-styles/stories/index.js create mode 100644 packages/icons/src/library/unseen.js create mode 100644 packages/plugins/src/components/test/plugin-area.js delete mode 100644 phpunit/block-supports/duotone-test.php create mode 100644 phpunit/blocks/render-block-navigation-submenu-test.php create mode 100644 phpunit/class-wp-duotone-test.php create mode 100644 phpunit/class-wp-get-block-css-selectors-test.php create mode 100644 storybook/package-styles/block-editor-ltr.lazy.scss create mode 100644 storybook/package-styles/block-editor-rtl.lazy.scss create mode 100644 storybook/package-styles/block-library-ltr.lazy.scss create mode 100644 storybook/package-styles/block-library-rtl.lazy.scss create mode 100644 storybook/package-styles/components-ltr.lazy.scss create mode 100644 storybook/package-styles/components-rtl.lazy.scss create mode 100644 storybook/package-styles/config.js create mode 100644 storybook/package-styles/edit-site-ltr.lazy.scss create mode 100644 storybook/package-styles/edit-site-rtl.lazy.scss create mode 100644 storybook/package-styles/format-library-ltr.lazy.scss create mode 100644 storybook/package-styles/format-library-rtl.lazy.scss delete mode 100644 storybook/style-ltr.lazy.scss delete mode 100644 storybook/style-rtl.lazy.scss create mode 100644 test/e2e/specs/site-editor/site-editor-url-navigation.spec.js create mode 100644 test/integration/fixtures/blocks/core__media-text__deprecated-v6-wide-default-align.html create mode 100644 test/integration/fixtures/blocks/core__media-text__deprecated-v6-wide-default-align.json create mode 100644 test/integration/fixtures/blocks/core__media-text__deprecated-v6-wide-default-align.parsed.json create mode 100644 test/integration/fixtures/blocks/core__media-text__deprecated-v6-wide-default-align.serialized.html create mode 100644 test/integration/helpers/integration-test-editor.js create mode 100644 test/integration/helpers/wait-for-store-resolvers.js create mode 100644 test/integration/helpers/with-fake-timers.js diff --git a/.github/ISSUE_TEMPLATE/New_release.md b/.github/ISSUE_TEMPLATE/New_release.md new file mode 100644 index 00000000000000..0b8fffeb26dfe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/New_release.md @@ -0,0 +1,52 @@ +--- +name: Gutenberg Release +about: A checklist for the Gutenberg plugin release process +--- + +This issue is to provide visibility on the progress of the release process of Gutenberg VERSION_NUMBER and to centralize any conversations about it. The ultimate goal of this issue is to keep the reference of the steps, resources, work, and conversations about this release so it can be helpful for the next contributors releasing a new Gutenberg version. + +- Gutenberg version to release: VERSION_NUMBER ([milestone](ADD_LINK)) +- Release Manager (a.k.a. Release Lead): +- Release Date VERSION_NUMBER RC: ADD DATE +- Release Date VERSION_NUMBER: ADD DATE +- Previous version change log (as a reference): [15.3](https://github.com/WordPress/gutenberg/releases/tag/v15.3.0) + +## Resources + +- đŸ“– Read: [Gutenberg Release Process](https://developer.wordpress.org/block-editor/contributors/code/release/) +- đŸ“– Read: [Leading a Gutenberg Plugin Release](https://codep2.wordpress.com/2021/07/22/leading-a-gutenberg-plugin-release/) +- đŸ“½ Watch: [Gutenberg Plugin: New Release Workflow](https://www.youtube.com/watch?v=TnSgJd3zpJY) +- đŸ“½ Watch: [Creating the Gutenberg plugin v12.0 Release Candidate](https://www.youtube.com/watch?v=FLkLHKecxWg) +- đŸ“½ Watch: [Gutenberg plugin v12.0.0 Release Party!](https://www.youtube.com/watch?v=4SDtpVPDsLc) + +## Checklist + +### RC Day - Wednesday, March 15 + +- [ ] _Optional:_ Attend `#core-editor` meeting (14:00UTC) +- [ ] Post a message in `#core-editor` channel to let folks know you are starting the RC release process +- [ ] Organize and Label PRs on the relevant milestone +- [ ] Start the release process by triggering the `rc` [worklow](https://developer.wordpress.org/block-editor/contributors/code/release/#running-workflow) +- [ ] [Update the created Draft Release accordingly](https://developer.wordpress.org/block-editor/contributors/code/release/#view-the-release-draft) +- [ ] Publish Release +- [ ] Announce in `#core-editor` channel that RC1 has been released and is ready for testing +- [ ] Ping any other relevant channels announcing that the RC is available +- [ ] Create Draft of Release post on Make Core blog _(initial draft in [Google doc](https://docs.google.com/document/d/1Hh25EYXSvRd5K45gq0VFN7yfN5l9om2FpX-_KqpVW7g/edit#))_ + +### Between RC and Release + +- [ ] [Backport to RC](https://github.com/WordPress/gutenberg/pulls?q=is%3Apr+label%3A%22Backport+to+Gutenberg+RC%22+is%3Aclosed) +- [ ] [Draft Release Post Highlights and Change Log](https://docs.google.com/document/d/1Hh25EYXSvRd5K45gq0VFN7yfN5l9om2FpX-_KqpVW7g/edit#) +- [ ] Get assets from [Design Team](https://make.wordpress.org/design/) for the post +- [ ] Reach out to Highlight Authors to draft sections _(if necessary)_ + +### Release Day - Wednesday, March 22 + +- [ ] Post a message in `#core-editor` channel to let folks know you are starting the release process +- [ ] Start the release process by triggering the `stable` [worklow](https://developer.wordpress.org/block-editor/contributors/code/release/#running-workflow) +- [ ] Update the created Draft Release accordingly. Typically by copy/pasting the last RC release notes and add any changes/updates as needed. +- [ ] Publish Release +- [ ] Trigger the update to the plugin directory - SVN _(get approval from member of Core team if necessary)_ +- [ ] Announce in `#core-editor` channel that the plugin has been released +- [ ] Reach out to other contributors to help get the post reviewed +- [ ] Publish Release post on Make Core blog diff --git a/.github/setup-node/action.yml b/.github/setup-node/action.yml new file mode 100644 index 00000000000000..22cb81618a1efb --- /dev/null +++ b/.github/setup-node/action.yml @@ -0,0 +1,46 @@ +name: 'Setup Node.js and install npm dependencies' +description: 'Configure Node.js and install npm dependencies while managing all aspects of caching.' +inputs: + node-version: + description: 'Optional. The Node.js version to use. When not specified, the version specified in .nvmrc will be used.' + required: false + type: string + +runs: + using: 'composite' + steps: + - name: Use desired version of Node.js + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + with: + node-version-file: '.nvmrc' + node-version: ${{ inputs.node-version }} + cache: npm + + - name: Get Node.js and npm version + id: node-version + run: | + echo "NODE_VERSION=$(node -v)" >> $GITHUB_OUTPUT + shell: bash + + - name: Cache node_modules + id: cache-node_modules + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + with: + path: '**/node_modules' + key: node_modules-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('package-lock.json') }} + + - name: Install npm dependencies + if: ${{ steps.cache-node_modules.outputs.cache-hit != 'true' }} + run: npm ci + shell: bash + + # On cache hit, we run the post-install script to match the native `npm ci` behavior. + # An example of this is to patch `node_modules` using patch-package. + - name: Post-install + if: ${{ steps.cache-node_modules.outputs.cache-hit == 'true' }} + run: | + # Run the post-install script for the root project. + npm run postinstall + # Run the post-install scripts for workspaces. + npx lerna run postinstall + shell: bash diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index 6e915d0ffddd27..9052f1689c9216 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -69,7 +69,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: token: ${{ secrets.GUTENBERG_TOKEN }} @@ -164,11 +164,11 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: ref: ${{ needs.bump-version.outputs.release_branch || github.ref }} - - name: Use desired version of NodeJS + - name: Use desired version of Node.js uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version-file: '.nvmrc' @@ -219,7 +219,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: fetch-depth: 2 ref: ${{ needs.bump-version.outputs.release_branch }} @@ -307,13 +307,13 @@ jobs: if: ${{ endsWith( needs.bump-version.outputs.new_version, '-rc.1' ) }} steps: - name: Checkout (for CLI) - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: path: main ref: trunk - name: Checkout (for publishing) - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: path: publish # Later, we switch this branch in the script that publishes packages. @@ -326,7 +326,7 @@ jobs: git config user.name "Gutenberg Repository Automation" git config user.email gutenberg@wordpress.org - - name: Setup Node (for CLI) + - name: Setup Node.js uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version-file: 'main/.nvmrc' diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 9ae0116ef1c79b..c4fdafb422c9b6 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -37,11 +37,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: fetch-depth: 1 - - name: Use desired version of NodeJS + - name: Use desired version of Node.js uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version-file: '.nvmrc' diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml index 304052d4297b87..8f76c072133d0c 100644 --- a/.github/workflows/create-block.yml +++ b/.github/workflows/create-block.yml @@ -20,20 +20,17 @@ jobs: strategy: fail-fast: false matrix: - node: [14] + node: ['14'] os: [macos-latest, ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node with: node-version: ${{ matrix.node }} - cache: npm - - name: npm install, build, format and lint + - name: Create block shell: bash - run: | - npm ci - bash ./bin/test-create-block.sh + run: bash ./bin/test-create-block.sh diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 24256e4266a46e..39748288c4d662 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -27,18 +27,13 @@ jobs: totalParts: [4] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node - - name: Npm install and build - run: | - npm ci - npm run build + - name: Npm build + run: npm run build - name: Install WordPress run: | @@ -76,18 +71,13 @@ jobs: totalParts: [2] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node - - name: Npm install and build - run: | - npm ci - npm run build + - name: Npm build + run: npm run build - name: Install Playwright dependencies run: | @@ -125,7 +115,7 @@ jobs: steps: # Checkout defaults to using the branch which triggered the event, which # isn't necessarily `trunk` (e.g. in the case of a merge). - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: ref: trunk @@ -137,19 +127,14 @@ jobs: name: flaky-tests-report path: flaky-tests - - name: Use desired version of NodeJS + - name: Setup Node.js and install dependencies if: ${{ steps.download_artifact.outcome == 'success' }} - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm + uses: ./.github/setup-node - - name: Npm install and build + - name: Npm build if: ${{ steps.download_artifact.outcome == 'success' }} # TODO: We don't have to build the entire project, just the action itself. - run: | - npm ci - npm run build:packages + run: npm run build:packages - name: Report flaky tests if: ${{ steps.download_artifact.outcome == 'success' }} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index f56a5ecf48e757..e1da61b72e263b 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -6,5 +6,5 @@ jobs: name: 'Validation' runs-on: ubuntu-latest steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index ed2dac34ddc939..9ced7ce0f4684e 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -28,38 +28,19 @@ jobs: name: Run performance tests runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' }} + env: + WP_ARTIFACTS_PATH: ${{ github.workspace }}/artifacts steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Npm install - run: | - npm ci - - - name: Install specific versions of the themes used in tests - run: | - npm run wp-env start - npm run wp-env -- run tests-cli "wp theme update twentytwentyone --version=1.7" - npm run wp-env -- run tests-cli "wp theme update twentytwentythree --version=1.0" - npm run wp-env stop + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node - name: Compare performance with trunk if: github.event_name == 'pull_request' run: ./bin/plugin/cli.js perf $GITHUB_SHA trunk --tests-branch $GITHUB_SHA - - name: Store performance measurements - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 - with: - name: perf-test-results - path: ./__test-results/*.json - - name: Compare performance with current WordPress Core and previous Gutenberg versions if: github.event_name == 'release' env: @@ -94,6 +75,13 @@ jobs: run: | ./bin/plugin/cli.js perf $(echo $BRANCHES | tr ',' ' ') --tests-branch $GITHUB_SHA --wp-version "$WP_VERSION" + - name: Archive performance results + if: success() + uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # v3.1.1 + with: + name: performance-results + path: ${{ env.WP_ARTIFACTS_PATH }}/*.performance-results.json + - name: Publish performance results if: github.event_name == 'push' env: @@ -104,8 +92,8 @@ jobs: - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - if: always() + if: failure() with: name: failures-artifacts - path: ./__test-results/artifacts + path: ${{ env.WP_ARTIFACTS_PATH }} if-no-files-found: ignore diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml index 6568d46989fe65..a9b36ba7f98b5c 100644 --- a/.github/workflows/publish-npm-packages.yml +++ b/.github/workflows/publish-npm-packages.yml @@ -30,13 +30,13 @@ jobs: environment: WordPress packages steps: - name: Checkout (for CLI) - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: path: main ref: trunk - name: Checkout (for publishing) - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: path: publish # Later, we switch this branch in the script that publishes packages. @@ -49,7 +49,7 @@ jobs: git config user.name "Gutenberg Repository Automation" git config user.email gutenberg@wordpress.org - - name: Setup Node (for CLI) + - name: Setup Node.js uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version-file: 'main/.nvmrc' diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml index 5d38f7965f7b10..56dce7a2825c9b 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -15,11 +15,11 @@ jobs: steps: # Checkout defaults to using the branch which triggered the event, which # isn't necessarily `trunk` (e.g. in the case of a merge). - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: ref: trunk - - name: Use desired version of NodeJS + - name: Use desired version of Node.js uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version: ${{ matrix.node }} diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index 0f1474c824a208..d04f312b9521bb 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -23,7 +23,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - name: Use desired version of Java uses: actions/setup-java@3f07048e3d294f56e9b90ac5ea2c6f74e9ad0f98 # v3.10.0 @@ -31,13 +31,8 @@ jobs: distribution: 'temurin' java-version: '11' - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - run: npm ci + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node - name: Gradle cache uses: gradle/gradle-build-action@6095a76664413da4c8c134ee32e8a8ae900f0f1f # v2.4.0 @@ -53,7 +48,7 @@ jobs: - name: Create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0 # v2.27.0 + uses: reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b # v2.28.0 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -64,7 +59,7 @@ jobs: script: echo "Generated AVD snapshot for caching." - name: Run tests - uses: reactivecircus/android-emulator-runner@50986b1464923454c95e261820bc626f38490ec0 # v2.27.0 + uses: reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b # v2.28.0 with: api-level: ${{ matrix.api-level }} force-avd-creation: false diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index 933b4184957d11..ff2c73e90ac689 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -23,15 +23,10 @@ jobs: native-test-name: [gutenberg-editor-rendering] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - run: npm ci + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node - name: Prepare build cache key run: find package-lock.json packages/react-native-editor/ios packages/react-native-aztec/ios packages/react-native-bridge/ios -type f -print0 | sort -z | xargs -0 shasum | tee ios-checksums.txt diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index 00002aaf52f191..3b457401560a8d 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -22,9 +22,9 @@ jobs: if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Use desired version of NodeJS + - name: Use desired version of Node.js uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version-file: '.nvmrc' diff --git a/.github/workflows/storybook-pages.yml b/.github/workflows/storybook-pages.yml index 616fad4e55b5aa..e21c1f14d8be21 100644 --- a/.github/workflows/storybook-pages.yml +++ b/.github/workflows/storybook-pages.yml @@ -12,18 +12,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: ref: trunk - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm - - - name: Install Dependencies - run: npm ci + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node - name: Build Storybook run: npm run storybook:build diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index ca9adb75ebca83..acb7417ac78ffb 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -31,21 +31,18 @@ jobs: node: ['14'] steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node with: node-version: ${{ matrix.node }} - cache: npm - - name: Npm install and build + - name: Npm build # It's not necessary to run the full build, since Jest can interpret # source files with `babel-jest`. Some packages have their own custom # build tasks, however. These must be run. - run: | - npm ci - npx lerna run build + run: npx lerna run build - name: Running the tests run: npm run test:unit -- --ci --maxWorkers=2 --cacheDirectory="$HOME/.jest-cache" @@ -77,13 +74,10 @@ jobs: WP_ENV_PHP_VERSION: ${{ matrix.php }} steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Set up Node.js - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node ## # This allows Composer dependencies to be installed using a single step. @@ -116,10 +110,8 @@ jobs: with: custom-cache-suffix: $(/bin/date -u --date='last Mon' "+%F") - - name: Install npm dependencies - run: | - npm ci - npm run build + - name: Npm build + run: npm run build - name: Docker debug information run: | @@ -166,7 +158,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - name: Set up PHP uses: shivammathur/setup-php@d30ad8b1843ace22e6698ab99bbafaa747b6bd0d # v2.24.0 @@ -233,21 +225,16 @@ jobs: if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} steps: - - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + - uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 - - name: Use desired version of NodeJS - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 - with: - node-version-file: '.nvmrc' - cache: npm + - name: Setup Node.js and install dependencies + uses: ./.github/setup-node - - name: Npm install and build + - name: Npm build # It's not necessary to run the full build, since Jest can interpret # source files with `babel-jest`. Some packages have their own custom # build tasks, however. These must be run. - run: | - npm ci - npx lerna run build + run: npx lerna run build - name: Running the tests run: npm run native test -- --ci --maxWorkers=2 --cacheDirectory="$HOME/.jest-cache" diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index 5cb432ee837ee3..392cf912e8db54 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 + uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0 with: ref: ${{ matrix.branch }} token: ${{ secrets.GUTENBERG_TOKEN }} diff --git a/bin/log-performance-results.js b/bin/log-performance-results.js index eac2f13d0ac977..9bc7ef9cb99b11 100755 --- a/bin/log-performance-results.js +++ b/bin/log-performance-results.js @@ -10,22 +10,25 @@ const [ token, branch, hash, baseHash, timestamp ] = process.argv.slice( 2 ); const resultsFiles = [ { - file: 'post-editor-performance-results.json', + file: 'post-editor.performance-results.json', metricsPrefix: '', }, { - file: 'front-end-block-theme-performance-results.json', + file: 'front-end-block-theme.performance-results.json', metricsPrefix: 'block-theme-', }, { - file: 'front-end-classic-theme-performance-results.json', + file: 'front-end-classic-theme.performance-results.json', metricsPrefix: 'classic-theme-', }, ]; const performanceResults = resultsFiles.map( ( { file } ) => JSON.parse( - fs.readFileSync( path.join( __dirname, '../' + file ), 'utf8' ) + fs.readFileSync( + path.join( process.env.WP_ARTIFACTS_PATH, file ), + 'utf8' + ) ) ); diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js index 75ab95a4cc4a10..0804c794e9d8e7 100644 --- a/bin/plugin/commands/performance.js +++ b/bin/plugin/commands/performance.js @@ -3,7 +3,7 @@ */ const fs = require( 'fs' ); const path = require( 'path' ); -const { mapValues, kebabCase } = require( 'lodash' ); +const { mapValues } = require( 'lodash' ); const SimpleGit = require( 'simple-git' ); /** @@ -18,6 +18,9 @@ const { } = require( '../lib/utils' ); const config = require( '../config' ); +const ARTIFACTS_PATH = + process.env.WP_ARTIFACTS_PATH || path.join( process.cwd(), 'artifacts' ); + /** * @typedef WPPerformanceCommandOptions * @@ -83,6 +86,17 @@ const config = require( '../config' ); * @property {number=} maxListViewOpen Max time to open list view. */ +/** + * Sanitizes branch name to be used in a path or a filename. + * + * @param {string} branch + * + * @return {string} Sanitized branch name. + */ +function sanitizeBranchName( branch ) { + return branch.replace( /[^a-zA-Z0-9-]/g, '-' ); +} + /** * Computes the average number from an array numbers. * @@ -182,38 +196,22 @@ function curateResults( testSuite, results ) { * @return {Promise} Performance results for the branch. */ async function runTestSuite( testSuite, performanceTestDirectory, runKey ) { - try { - await runShellScript( - `npm run test:performance -- packages/e2e-tests/specs/performance/${ testSuite }.test.js`, - performanceTestDirectory - ); - } catch ( error ) { - fs.mkdirSync( './__test-results/artifacts', { recursive: true } ); - const artifactsFolder = path.join( - performanceTestDirectory, - 'artifacts/' - ); - await runShellScript( - 'cp -Rv ' + artifactsFolder + ' ' + './__test-results/artifacts/' - ); + const resultsFilename = `${ runKey }.performance-results.json`; - throw error; - } - - const resultsFile = path.join( + await runShellScript( + `npm run test:performance -- ${ testSuite }`, performanceTestDirectory, - `packages/e2e-tests/specs/performance/${ testSuite }.test.results.json` - ); - fs.mkdirSync( './__test-results', { recursive: true } ); - fs.copyFileSync( resultsFile, `./__test-results/${ runKey }.results.json` ); - const rawResults = await readJSONFile( - path.join( - performanceTestDirectory, - `packages/e2e-tests/specs/performance/${ testSuite }.test.results.json` - ) + { + ...process.env, + WP_ARTIFACTS_PATH: ARTIFACTS_PATH, + RESULTS_FILENAME: resultsFilename, + } ); - return curateResults( testSuite, rawResults ); + return curateResults( + testSuite, + await readJSONFile( path.join( ARTIFACTS_PATH, resultsFilename ) ) + ); } /** @@ -299,8 +297,8 @@ async function runPerformanceTests( branches, options ) { const branchDirectories = {}; for ( const branch of branches ) { log( ` >> Branch: ${ branch }` ); - const environmentDirectory = - rootDirectory + '/envs/' + kebabCase( branch ); + const sanitizedBranch = sanitizeBranchName( branch ); + const environmentDirectory = rootDirectory + '/envs/' + sanitizedBranch; // @ts-ignore branchDirectories[ branch ] = environmentDirectory; const buildPath = `${ environmentDirectory }/plugin`; @@ -328,15 +326,41 @@ async function runPerformanceTests( branches, options ) { buildPath ); - await runShellScript( - 'cp ' + - path.resolve( - performanceTestDirectory, - 'bin/plugin/utils/.wp-env.performance.json' - ) + - ' ' + - environmentDirectory + - '/.wp-env.json' + // Create the config file for the current env. + fs.writeFileSync( + path.join( environmentDirectory, '.wp-env.json' ), + JSON.stringify( + { + core: 'WordPress/WordPress', + plugins: [ path.join( environmentDirectory, 'plugin' ) ], + themes: [ + path.join( + performanceTestDirectory, + 'test/emptytheme' + ), + 'https://downloads.wordpress.org/theme/twentytwentyone.1.7.zip', + 'https://downloads.wordpress.org/theme/twentytwentythree.1.0.zip', + ], + env: { + tests: { + mappings: { + 'wp-content/mu-plugins': path.join( + performanceTestDirectory, + 'packages/e2e-tests/mu-plugins' + ), + 'wp-content/plugins/gutenberg-test-plugins': + path.join( + performanceTestDirectory, + 'packages/e2e-tests/plugins' + ), + }, + }, + }, + }, + null, + 2 + ), + 'utf8' ); if ( options.wpVersion ) { @@ -393,32 +417,39 @@ async function runPerformanceTests( branches, options ) { /** @type {Record>} */ const results = {}; + const wpEnvPath = path.join( + performanceTestDirectory, + 'node_modules/.bin/wp-env' + ); + for ( const testSuite of testSuites ) { results[ testSuite ] = {}; /** @type {Array>} */ const rawResults = []; - // Alternate three times between branches. for ( let i = 0; i < TEST_ROUNDS; i++ ) { + const roundInfo = `round ${ i + 1 } of ${ TEST_ROUNDS }`; + log( ` >> Suite: ${ testSuite } (${ roundInfo })` ); rawResults[ i ] = {}; for ( const branch of branches ) { - const runKey = `${ branch }_${ testSuite }_run-${ i }`; + const sanitizedBranch = sanitizeBranchName( branch ); + const runKey = `${ testSuite }_${ sanitizedBranch }_run-${ i }`; // @ts-ignore const environmentDirectory = branchDirectories[ branch ]; - log( ` >> Branch: ${ branch }, Suite: ${ testSuite }` ); - log( ' >> Starting the environment.' ); + log( ` >> Branch: ${ branch }` ); + log( ' >> Starting the environment.' ); await runShellScript( - '../../tests/node_modules/.bin/wp-env start', + `${ wpEnvPath } start`, environmentDirectory ); - log( ' >> Running the test.' ); + log( ' >> Running the test.' ); rawResults[ i ][ branch ] = await runTestSuite( testSuite, performanceTestDirectory, runKey ); - log( ' >> Stopping the environment' ); + log( ' >> Stopping the environment' ); await runShellScript( - '../../tests/node_modules/.bin/wp-env stop', + `${ wpEnvPath } stop`, environmentDirectory ); } @@ -481,9 +512,9 @@ async function runPerformanceTests( branches, options ) { ); console.table( invertedResult ); - const resultsFilename = testSuite + '-performance-results.json'; + const resultsFilename = testSuite + '.performance-results.json'; fs.writeFileSync( - path.resolve( __dirname, '../../../', resultsFilename ), + path.join( ARTIFACTS_PATH, resultsFilename ), JSON.stringify( results[ testSuite ], null, 2 ) ); } diff --git a/bin/plugin/lib/utils.js b/bin/plugin/lib/utils.js index 4a75437a60694e..c50094321710ca 100644 --- a/bin/plugin/lib/utils.js +++ b/bin/plugin/lib/utils.js @@ -33,6 +33,7 @@ function runShellScript( script, cwd, env = {} ) { NO_CHECKS: 'true', PATH: process.env.PATH, HOME: process.env.HOME, + USER: process.env.USER, ...env, }, }, diff --git a/bin/plugin/utils/.wp-env.performance.json b/bin/plugin/utils/.wp-env.performance.json deleted file mode 100644 index 112c2684f64a48..00000000000000 --- a/bin/plugin/utils/.wp-env.performance.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "core": "WordPress/WordPress", - "plugins": [ "./plugin" ], - "themes": [ "../../tests/test/emptytheme" ], - "env": { - "tests": { - "mappings": { - "wp-content/mu-plugins": "../../tests/packages/e2e-tests/mu-plugins", - "wp-content/plugins/gutenberg-test-plugins": "../../tests/packages/e2e-tests/plugins" - } - } - } -} diff --git a/changelog.txt b/changelog.txt index a0ce9ad1790f03..7683dfcb0d3e73 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,326 @@ == Changelog == += 15.4.0 = + +## Changelog + +### Enhancements + +#### Site Editor + +- Adjust whileHover effect to be a bit subtler and less pronounced. ([48928](https://github.com/WordPress/gutenberg/pull/48928)) +- Go direct to edit from manage all templates list. ([48764](https://github.com/WordPress/gutenberg/pull/48764)) +- Move "Add Template"'s descriptions to tooltips. ([48710](https://github.com/WordPress/gutenberg/pull/48710)) +- Add descriptions to all panels in the Site Editor's dark side. ([48739](https://github.com/WordPress/gutenberg/pull/48739)) +- Add hover animation to site editor canvas. ([48575](https://github.com/WordPress/gutenberg/pull/48575)) +- Fix non-us spelling in sidebar. ([48976](https://github.com/WordPress/gutenberg/pull/48976)) +- Prevent the saving button from showing when renaming templates. ([48399](https://github.com/WordPress/gutenberg/pull/48399)) +- Navigation Sidebar: Change the logic about which navigation gets selected for the sidebar. ([48689](https://github.com/WordPress/gutenberg/pull/48689)) +- Add "Added by" description to template part navigation sidebar. ([48732](https://github.com/WordPress/gutenberg/pull/48732)) +- Add border radius to off canvas navigation menu items. ([48798](https://github.com/WordPress/gutenberg/pull/48798)) +- Add page details when viewing a specific page. ([48650](https://github.com/WordPress/gutenberg/pull/48650)) +- Duotone: Limit SVG filter output to used filters. ([48995](https://github.com/WordPress/gutenberg/pull/48995)) +- Hide navigation screen in site editor. ([49043](https://github.com/WordPress/gutenberg/pull/49043)) + +#### Block Library + +- Open convert to links modal on select of a page item. ([48723](https://github.com/WordPress/gutenberg/pull/48723)) +- Post Featured Image: Remove 16:10. ([48969](https://github.com/WordPress/gutenberg/pull/48969)) +- Cover: Add constrained/flow layout. ([45326](https://github.com/WordPress/gutenberg/pull/45326)) +- Cover: Add text color block support. ([41572](https://github.com/WordPress/gutenberg/pull/41572)) + +#### Components + +- FontSizePicker: Allow custom units. ([48468](https://github.com/WordPress/gutenberg/pull/48468)) +- `Navigator`: Disable initial animation. ([49062](https://github.com/WordPress/gutenberg/pull/49062)) +- Try: Update Tertiary Button appearance. ([48888](https://github.com/WordPress/gutenberg/pull/48888)) +- FormTokenField: Hide suggestions list on blur event if input value is invalid. ([48785](https://github.com/WordPress/gutenberg/pull/48785)) + +#### Design Tools + +- Block Supports: Add text columns (column count) to typography block supports. ([33587](https://github.com/WordPress/gutenberg/pull/33587)) + +#### Global Styles + +- Move the global styles provider to the app level component. ([49011](https://github.com/WordPress/gutenberg/pull/49011)) +- Add support for `:link` and `:Any-link` in `theme.json`. ([48634](https://github.com/WordPress/gutenberg/pull/48634)) +- Add compound class to layout wrapper for global spacing styles. ([47952](https://github.com/WordPress/gutenberg/pull/47952)) + +#### Block API + +- Block Deprecations: Provide extra data for isEligible check. ([48815](https://github.com/WordPress/gutenberg/pull/48815)) + +#### Post Editor + +- Provide static native editor help article slugs. ([48802](https://github.com/WordPress/gutenberg/pull/48802)) +- Try getting Post Content layout on server before editor loads. ([45299](https://github.com/WordPress/gutenberg/pull/45299)) + +#### Packages + +- Introduce prependHTTPS URL util. ([47648](https://github.com/WordPress/gutenberg/pull/47648)) + +### Bug Fixes + +#### Block Library + +- Embed Block: Fix Aspect Ratio Classes #29641. ([41141](https://github.com/WordPress/gutenberg/pull/41141)) +- Ensure aspect ratio is applied when Post Featured Image block is linked. ([48495](https://github.com/WordPress/gutenberg/pull/48495)) +- Fix PostContent initial render by waiting for the canEdit request. ([48642](https://github.com/WordPress/gutenberg/pull/48642)) +- Fix classic menu fallback race condition. ([48811](https://github.com/WordPress/gutenberg/pull/48811)) +- Fix navigation block off-canvas appender for empty menus. ([48907](https://github.com/WordPress/gutenberg/pull/48907)) +- Fixes extra UI in navigation block inspector. ([48679](https://github.com/WordPress/gutenberg/pull/48679)) +- Import Classic Menu using the menu name as the block menu title. ([48771](https://github.com/WordPress/gutenberg/pull/48771)) +- Navigation Link: Remove color generation code. ([48927](https://github.com/WordPress/gutenberg/pull/48927)) +- Navigation: Fix missing state for MenuControls. ([48921](https://github.com/WordPress/gutenberg/pull/48921)) +- Update missing translation from label. ([48760](https://github.com/WordPress/gutenberg/pull/48760)) +- Widget Importer: Fix Widget Group block imports. ([48669](https://github.com/WordPress/gutenberg/pull/48669)) +- Query Loop: Show variant patterns even if there are no patterns for the Query Loop block. ([48793](https://github.com/WordPress/gutenberg/pull/48793)) +- Comments: Fix 'sprintf requires more than 1 params' error. ([49054](https://github.com/WordPress/gutenberg/pull/49054)) +- Adjust Post Featured Image PanelBody label to "Settings". ([49076](https://github.com/WordPress/gutenberg/pull/49076)) +- Add help text to Gallery Image Size control. ([49074](https://github.com/WordPress/gutenberg/pull/49074)) +- Comments Block (Legacy): Update missing translation. ([48820](https://github.com/WordPress/gutenberg/pull/48820)) +- I18n of created Navigation menu title. ([48773](https://github.com/WordPress/gutenberg/pull/48773)) +- Make sure the directly inserted block in the Nav block is a Page link. ([48740](https://github.com/WordPress/gutenberg/pull/48740)) +- Navigation Link: Don't remove 'block_core_navigation_link_build_css_colors'. ([49064](https://github.com/WordPress/gutenberg/pull/49064)) +- Navigation: Don't save the level of the link in an attribute. ([48219](https://github.com/WordPress/gutenberg/pull/48219)) +- Refactor away state in Nav menu selector. ([45464](https://github.com/WordPress/gutenberg/pull/45464)) +- Revert: Navigation: Always create a fallback menu. ([48602](https://github.com/WordPress/gutenberg/pull/48602)) +- Tweak Latest Posts block PanelBody labels. ([49079](https://github.com/WordPress/gutenberg/pull/49079)) +- Tweak label for Latest Posts excerpt control. ([49077](https://github.com/WordPress/gutenberg/pull/49077)) +- Page List Block: Show untitled pages on page list on the editor. ([48772](https://github.com/WordPress/gutenberg/pull/48772)) + +#### Site Editor + +- Don't offer Classic block as a recovery action when not registered. ([49051](https://github.com/WordPress/gutenberg/pull/49051)) +- Fix browser history when synchronising state with urls. ([48731](https://github.com/WordPress/gutenberg/pull/48731)) +- Fix lingering insertion point within template parts. ([48913](https://github.com/WordPress/gutenberg/pull/48913)) +- Fix template part actions in List View. ([48905](https://github.com/WordPress/gutenberg/pull/48905)) +- Fix text alignment in the Site Editor sidebar. ([48959](https://github.com/WordPress/gutenberg/pull/48959)) +- Fix typo in template parts description. ([48781](https://github.com/WordPress/gutenberg/pull/48781)) +- Fix browse mode descriptions margin. ([48778](https://github.com/WordPress/gutenberg/pull/48778)) +- Fix scrollbar in site editor. ([48822](https://github.com/WordPress/gutenberg/pull/48822)) +- Site Editor Navigation panel: Update appearance of non-link blocks. ([48933](https://github.com/WordPress/gutenberg/pull/48933)) +- Navigation sidebar shows a wrong submenu popover. ([48941](https://github.com/WordPress/gutenberg/pull/48941)) +- Show creation popover on empty page links in the navigation sidebar. ([48746](https://github.com/WordPress/gutenberg/pull/48746)) +- Site button metrics. ([48918](https://github.com/WordPress/gutenberg/pull/48918)) +- Remove actions from SidebarNavigationScreenWrapper. ([48935](https://github.com/WordPress/gutenberg/pull/48935)) +- Update template descriptions with more detail. ([48934](https://github.com/WordPress/gutenberg/pull/48934)) + +#### Global Styles + +- Fix typo: Use WP_Theme_JSON_Gutenberg instead of WP_Theme_JSON class name. ([48648](https://github.com/WordPress/gutenberg/pull/48648)) +- Fix: Crashes on getNodesWithSettings and getNodesWithStyles. ([49023](https://github.com/WordPress/gutenberg/pull/49023)) +- Fix: Global Styles crash in updateConfigWithSeparator when not block styles are passed. ([49045](https://github.com/WordPress/gutenberg/pull/49045)) +- Fix: Global Styles getNodesWithStyles expects an object with elements. ([49044](https://github.com/WordPress/gutenberg/pull/49044)) +- Fix: Global Styles getPresetsClasses crashes if no selector is passed. ([49024](https://github.com/WordPress/gutenberg/pull/49024)) +- Fix: Global styles forces a white background. ([49042](https://github.com/WordPress/gutenberg/pull/49042)) +- Style Book: Move iframe to root of content area to support styles that overflow block previews. ([48664](https://github.com/WordPress/gutenberg/pull/48664)) +- `WP_Theme_JSON`: Sync indirect properties changes from core. ([48646](https://github.com/WordPress/gutenberg/pull/48646)) + +#### Components + +- Fix HStack and VStack alignment prop. ([47914](https://github.com/WordPress/gutenberg/pull/47914)) +- ResizeTooltip: Use default.fontFamily on tooltip. ([48805](https://github.com/WordPress/gutenberg/pull/48805)) +- ResponsiveWrapper: Use aspect-ratio CSS prop and support SVG elements. ([48573](https://github.com/WordPress/gutenberg/pull/48573)) + +#### Accessibility + +- Make sure useFocusOnMount runs when all the children tabbable elements have mounted. ([42187](https://github.com/WordPress/gutenberg/pull/42187)) +- Manage selection on block sync. ([48979](https://github.com/WordPress/gutenberg/pull/48979)) + +#### Post Editor + +- Distraction Free Mode: Don't show the metaboxes. ([48947](https://github.com/WordPress/gutenberg/pull/48947)) +- Don't add Post Content layout styles to title in the post editor. ([48663](https://github.com/WordPress/gutenberg/pull/48663)) +- Fix animation and browser console error when returning from template edit mode. ([48930](https://github.com/WordPress/gutenberg/pull/48930)) + +#### Block Editor + +- LinkControl: Remove HTML from suggestion title before passing it to TextHighlight component. ([48685](https://github.com/WordPress/gutenberg/pull/48685)) +- Order initial block items in Navigation with PrivateInserter. ([48752](https://github.com/WordPress/gutenberg/pull/48752)) +- BlockInvalidWarning: Prefer `canInsertBlockType` and refactor to hooks. ([49052](https://github.com/WordPress/gutenberg/pull/49052)) +- Fix grouping actions in List View. ([48910](https://github.com/WordPress/gutenberg/pull/48910)) +- Fix typo in the media-categories component. ([49047](https://github.com/WordPress/gutenberg/pull/49047)) +- Custom link UI does appears outside canvas on the sidebar navigation. ([48633](https://github.com/WordPress/gutenberg/pull/48633)) +- Use proper color for block styles control. ([46684](https://github.com/WordPress/gutenberg/pull/46684)) +- Update Welcome Guide article links to avoid redirect. ([48582](https://github.com/WordPress/gutenberg/pull/48582)) +- Columns Block: Don't show the column count change UI when `templateLock` is `all`. ([48691](https://github.com/WordPress/gutenberg/pull/48691)) +- Remove border from quick inserter child elements. ([48794](https://github.com/WordPress/gutenberg/pull/48794)) + +#### Inspector Controls +- Fix settings tab active state border in block inspector. ([48945](https://github.com/WordPress/gutenberg/pull/48945)) + +#### Testing + +- Playwright Utils: Fix the 'publishPost' address locator. ([48729](https://github.com/WordPress/gutenberg/pull/48729)) + +#### CSS & Styling +- Fix duplication of block classname in feature selectors for style variations. ([48662](https://github.com/WordPress/gutenberg/pull/48662)) + +#### Experimental + +- Fix KSES filter for un-prettified filters. ([49004](https://github.com/WordPress/gutenberg/pull/49004)) + +#### Packages + +- Rich text: Fix range equality checks for Safari. ([48733](https://github.com/WordPress/gutenberg/pull/48733)) +- Preferences Modal: Fix double focus outline in tab item. ([48996](https://github.com/WordPress/gutenberg/pull/48996)) + +#### Tools + +- Scripts: Fix `render.php` isn't copied in Windows OS. ([48735](https://github.com/WordPress/gutenberg/pull/48735)) + +#### Mobile + +- Mobile - Fix parsing of CSS units for null matched values. ([48484](https://github.com/WordPress/gutenberg/pull/48484)) + +### Performance + +#### Block Editor + +- Rich text: useAnchor: Remove value dependency. ([48715](https://github.com/WordPress/gutenberg/pull/48715)) + +#### Post Editor + +- Lodash: Refactor away from `_.kebabCase()` in `EditorHelpTopics`. ([48776](https://github.com/WordPress/gutenberg/pull/48776)) +- Lodash: Refactor away from `edit-post` package. ([48786](https://github.com/WordPress/gutenberg/pull/48786)) + +#### Site Editor + +- Improve the Navigation panel's menu query. ([48908](https://github.com/WordPress/gutenberg/pull/48908)) +- Improve Site Editor performance tests. ([48138](https://github.com/WordPress/gutenberg/pull/48138)) + +#### Testing + +- Lodash: Remove from e2e-tests package. ([48775](https://github.com/WordPress/gutenberg/pull/48775)) + +#### Themes + +- Fix: Incorrect selector generated by `append_to_selector` method. ([48759](https://github.com/WordPress/gutenberg/pull/48759)) + +#### Block Library + +- Lodash: Remove `_.get()` from various blocks. ([48491](https://github.com/WordPress/gutenberg/pull/48491)) + +#### Data Layer + +- Lodash: Refactor away from `_.set()` in core-data. ([48784](https://github.com/WordPress/gutenberg/pull/48784)) + +#### GitHub Actions + +- Prefer committer over author date for perf results timestamp. ([48673](https://github.com/WordPress/gutenberg/pull/48673)) + +### Documentation + +- Add links to hook documentation in curation doc. ([48653](https://github.com/WordPress/gutenberg/pull/48653)) +- Add missing playwright end-to-end documentation to toc.json. ([48447](https://github.com/WordPress/gutenberg/pull/48447)) +- Adding examples of how to programmatically remove the panels in Document sidebar. ([48895](https://github.com/WordPress/gutenberg/pull/48895)) +- Adds link to post on the developer blog to the deprecation page. ([49069](https://github.com/WordPress/gutenberg/pull/49069)) +- Add position: Sticky to the Opt-in into UI controls appearanceTools section. ([48763](https://github.com/WordPress/gutenberg/pull/48763)) +- Fix broken Lerna documentation link. ([48890](https://github.com/WordPress/gutenberg/pull/48890)) +- Table of styles keys with since versions. ([48265](https://github.com/WordPress/gutenberg/pull/48265)) +- Fix URL mismatch. ([48931](https://github.com/WordPress/gutenberg/pull/48931)) +- Theme JSON schema: Add sticky position to settings, minHeight to styles. ([48948](https://github.com/WordPress/gutenberg/pull/48948)) +- Update the end-to-end tests documentation. ([48951](https://github.com/WordPress/gutenberg/pull/48951)) +- jest-preset-default: Update README to reflect current status. ([48925](https://github.com/WordPress/gutenberg/pull/48925)) + +### Code Quality + +#### Components + +- Autocomplete: Refactor to TypeScript. ([47751](https://github.com/WordPress/gutenberg/pull/47751)) +- Navigation: Refactor to TypeScript. ([48742](https://github.com/WordPress/gutenberg/pull/48742)) +- SelectControl: Improve prop types for single vs multiple selection. ([47390](https://github.com/WordPress/gutenberg/pull/47390)) +- `DimensionControl(Experimental)`: Refactor to TypeScript. ([47351](https://github.com/WordPress/gutenberg/pull/47351)) +- `Guide`: Refactor to TypeScript. ([47493](https://github.com/WordPress/gutenberg/pull/47493)) +- `Icon`: Refactor tests to TypeScript. ([49066](https://github.com/WordPress/gutenberg/pull/49066)) +- `PaletteEdit`: Refactor away from `lodash.kebabCase`. ([48637](https://github.com/WordPress/gutenberg/pull/48637)) +- `QueryControls`: Refactor away from `lodash.groupBy`. ([48779](https://github.com/WordPress/gutenberg/pull/48779)) +- components/utils/font: Refactor away from lodash `.get`. ([48629](https://github.com/WordPress/gutenberg/pull/48629)) +- remove lodash from `context/getStyledClassName`:. ([48688](https://github.com/WordPress/gutenberg/pull/48688)) +- withSpokenMessages: Change js files to typescript. ([48163](https://github.com/WordPress/gutenberg/pull/48163)) + +#### Block Library + +- Add Nav block files to those triggering error for exhaustive deps. ([48821](https://github.com/WordPress/gutenberg/pull/48821)) +- Fix Nav block exhaustive deps warnings. ([48680](https://github.com/WordPress/gutenberg/pull/48680)) +- Media Text: Refactored constants to it's designated file. ([48480](https://github.com/WordPress/gutenberg/pull/48480)) +- Navigation: Simplify the method for finding the fallback menu. ([48916](https://github.com/WordPress/gutenberg/pull/48916)) +- Duotone.php code cleanup. ([48607](https://github.com/WordPress/gutenberg/pull/48607)) +- Revert "Duotone: Limit SVG filter output to used filters". ([49102](https://github.com/WordPress/gutenberg/pull/49102)) + +#### Block Editor + +- Inserter: Remove outer scope values dependencies. ([48961](https://github.com/WordPress/gutenberg/pull/48961)) +- Inserter: Remove unnecessary dependency 'delayedFilterValue'. ([48960](https://github.com/WordPress/gutenberg/pull/48960)) +- Remove unused CSS from LinkControl Apply button. ([48431](https://github.com/WordPress/gutenberg/pull/48431)) +- Custom Classname block support: Update code comments to remove reference to anchor id. ([48709](https://github.com/WordPress/gutenberg/pull/48709)) +- List View: Remove unused selector from the 'useBlockSelection' hook. ([48984](https://github.com/WordPress/gutenberg/pull/48984)) +- Renames parent selection boolean param and improves documentation. ([48677](https://github.com/WordPress/gutenberg/pull/48677)) +- Tests: Cleanup unnecessary jest timers setup. ([49030](https://github.com/WordPress/gutenberg/pull/49030)) +- Avoid declaring a function inside another function. ([49049](https://github.com/WordPress/gutenberg/pull/49049)) + +#### Global Styles + +- Theme JSON: Clarify status of fixed position opt-in in appearance tools. ([48660](https://github.com/WordPress/gutenberg/pull/48660)) +- Extract a BorderPanel component as a reusable component between Global Styles and Block Inspector. ([48636](https://github.com/WordPress/gutenberg/pull/48636)) + +#### Data Layer + +- Data: Use real timers for private APIs tests. ([49029](https://github.com/WordPress/gutenberg/pull/49029)) + +#### Packages + +- Preferences: Remove `types` field from `package.json`. ([49053](https://github.com/WordPress/gutenberg/pull/49053)) +- Upgrade `typescript` to 4.9.5. ([48299](https://github.com/WordPress/gutenberg/pull/48299)) +- Compose: Remove useAsyncList from mobile exports. ([48241](https://github.com/WordPress/gutenberg/pull/48241)) +- Animation: Refactor to TypeScript. ([47042](https://github.com/WordPress/gutenberg/pull/47042)) +- PanelBody: Convert to TypeScript. ([47702](https://github.com/WordPress/gutenberg/pull/47702)) +- Refactor ToolbarContext to TS. ([49002](https://github.com/WordPress/gutenberg/pull/49002)) +- Refactor/toolbar button component to typescript. ([47750](https://github.com/WordPress/gutenberg/pull/47750)) +- `PaletteEdit`: Convert to TypeScript. ([47764](https://github.com/WordPress/gutenberg/pull/47764)) +- navigateRegions: Convert to TypeScript. ([48632](https://github.com/WordPress/gutenberg/pull/48632)) +- withFallbackStyles: Convert to TypeScript. ([48720](https://github.com/WordPress/gutenberg/pull/48720)) +- withFilters: Convert to TypeScript. ([48721](https://github.com/WordPress/gutenberg/pull/48721)) +- withFocusReturn: Convert to TypeScript. ([48748](https://github.com/WordPress/gutenberg/pull/48748)) +- withNotices: Convert to TypeScript. ([49088](https://github.com/WordPress/gutenberg/pull/49088)) +- Packages: Remove completely two deprecated webpack plugins. ([48770](https://github.com/WordPress/gutenberg/pull/48770)) + +### Tools + +- Env: Fix typo / grammar README.md. ([48952](https://github.com/WordPress/gutenberg/pull/48952)) +- ci: Add Rich Text code owner. ([48727](https://github.com/WordPress/gutenberg/pull/48727)) + +#### Testing + +- Add `pageUtils.pressKeys` to playwright utils. ([49009](https://github.com/WordPress/gutenberg/pull/49009)) +- Add artifacts upload for the performance tests. ([48243](https://github.com/WordPress/gutenberg/pull/48243)) +- Fix flaky block hierarchy navigation test by better inserter selection. ([48780](https://github.com/WordPress/gutenberg/pull/48780)) +- Migrate multi-block selection end-to-end tests to Playwright. ([48035](https://github.com/WordPress/gutenberg/pull/48035)) +- Navigation block end-to-end tests: Default to my most recently created menu. ([48132](https://github.com/WordPress/gutenberg/pull/48132)) +- Upgrade Jest from 27 to 29.5.0. ([47388](https://github.com/WordPress/gutenberg/pull/47388)) +- Duotone: Style Engine: Add unit test and associated refactoring. ([49033](https://github.com/WordPress/gutenberg/pull/49033)) +- Query Block: Add tests for `getValueFromObjectPath()` util. ([48956](https://github.com/WordPress/gutenberg/pull/48956)) + +## First time contributors + +The following PRs were merged by first time contributors: + +- @anver: Env: Fix typo / grammar README.md. ([48952](https://github.com/WordPress/gutenberg/pull/48952)) +- @bhavz-10: withSpokenMessages: Change js files to typescript. ([48163](https://github.com/WordPress/gutenberg/pull/48163)) +- @krishneup: Update missing translation from label. ([48760](https://github.com/WordPress/gutenberg/pull/48760)) +- @mike-day: Refactor/toolbar button component to typescript. ([47750](https://github.com/WordPress/gutenberg/pull/47750)) +- @shvlv: FormTokenField: Hide suggestions list on blur event if input value is invalid. ([48785](https://github.com/WordPress/gutenberg/pull/48785)) +- @TylerB24890: Embed Block: Fix Aspect Ratio Classes #29641. ([41141](https://github.com/WordPress/gutenberg/pull/41141)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @ajlende @andrewserong @anver @aristath @bhavz-10 @brookewp @chad1008 @ciampo @DaisyOlsen @dcalhoun @draganescu @ellatrix @fabiankaegy @felixarntz @flootr @fluiddot @geriux @getdave @glendaviesnz @gvgvgvijayan @gziolo @hideokamoto @jameskoster @jasmussen @jeryj @jorgefilipecosta @jsnajdr @kevin940726 @kienstra @krishneup @MaggieCabrera @Mamaduka @mburridge @mike-day @mirka @mtias @ndiego @ntsekouras @oandregal @Rahmon @richtabor @ryanwelcher @SavPhill @scruffian @shvlv @SiobhyB @swissspidy @t-hamano @talldan @tellthemachines @tomdevisser @TylerB24890 @tyxla @WunderBart @youknowriad + + = 15.3.1 = @@ -5156,7 +5477,7 @@ The following contributors merged PRs in this release: - Docs: Fix required status of `onSelectUrl` prop of `MediaReplaceFlow` component. ([44025](https://github.com/WordPress/gutenberg/pull/44025)) - Document new global styles filters. ([44111](https://github.com/WordPress/gutenberg/pull/44111)) - Document template_lock=noContent for Custom Post Types. ([43977](https://github.com/WordPress/gutenberg/pull/43977)) -- InnerBlocks: document that `templateLock:NoContent` cannot be overriden by children. ([43825](https://github.com/WordPress/gutenberg/pull/43825)) +- InnerBlocks: document that `templateLock:NoContent` cannot be overridden by children. ([43825](https://github.com/WordPress/gutenberg/pull/43825)) - Theme.json: Add default values for settings.spacing.spacingScale. ([43860](https://github.com/WordPress/gutenberg/pull/43860)) - Theme.json: Fix schema for useRootPaddingAwareAlignments. ([43628](https://github.com/WordPress/gutenberg/pull/43628)) - Typography block supports: Call tear_down in tests and format PHP doc blocks. ([43968](https://github.com/WordPress/gutenberg/pull/43968)) diff --git a/docs/README.md b/docs/README.md index 4d5343a76141e9..49e562031f9e51 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,32 +1,34 @@ # Block Editor Handbook -**Gutenberg** is a codename for a whole new paradigm in WordPress site building and publishing, that aims to revolutionize the entire publishing experience as much as Gutenberg did the printed word. The project is right now in the second phase of a four-phase process that will touch every piece of WordPress -- Editing, **Customization** (which includes Full Site Editing, Block Patterns, Block Directory and Block based themes), Collaboration, and Multilingual -- and is focused on a new editing experience, the block editor (which is the topic of the current documentation). +**Gutenberg** is a codename for a whole new paradigm in WordPress site building and publishing, that aims to revolutionize the entire publishing experience as much as Gutenberg did the printed word. Right now, the project is in the second phase of a four-phase process that will touch every piece of WordPress -- Editing, **Customization** (which includes Full Site Editing, Block Patterns, Block Directory and Block based themes), Collaboration, and Multilingual -- and is focused on a new editing experience, the block editor (which is the topic of the current documentation). ![Quick view of the block editor](https://raw.githubusercontent.com/WordPress/gutenberg/trunk/docs/assets/quick-view-of-the-block-editor.png) -**Legend :** +**Legend:** -1. Block Inserter +1. Block inserter 2. Block editor content area -3. Settings Sidebar +3. Settings sidebar -Using a system of Blocks to compose and format content, the new block-based editor is designed to create rich, flexible layouts for websites and digital products. Content is created in the unit of blocks instead of freeform text with inserted media, embeds and Shortcodes (there's a Shortcode block though). +Using a system of Blocks to compose and format content, the new block-based editor is designed to create rich, flexible layouts for websites and digital products. Content is created using blocks instead of freeform text with inserted media, embeds and Shortcodes (there's a Shortcode block, though). -Blocks treat Paragraphs, Headings, Media, and Embeds all as components that, when strung together, make up the content stored in the WordPress database, replacing the traditional concept of freeform text with embedded media and shortcodes. The new editor is designed with progressive enhancement, meaning that it is back-compatible with all legacy content, and it also offers a process to try to convert and split a Classic block into equivalent blocks using client-side parsing. Finally, the blocks offer enhanced editing and format controls. +Blocks treat Paragraphs, Headings, Media, and Embeds all as components that, when strung together, make up the content stored in the WordPress database, replacing the traditional concept of freeform text with embedded media and shortcodes. The new editor is designed with progressive enhancement, meaning that it is backward compatible with all legacy content. It also offers a process to try to convert and split a Classic block into equivalent blocks using client-side parsing. Finally, the blocks offer enhanced editing and format controls. -The Editor offers rich new value to users with visual, drag-and-drop creation tools and powerful developer enhancements with modern vendor packages, reusable components, rich APIs and hooks to modify and extend the editor through Custom Blocks, Custom Block Styles and Plugins. - -[Learn to use the block editor](https://wordpress.org/support/article/wordpress-editor/) to create media-rich posts and pages. +The Editor offers rich new value to users by offering visual, drag-and-drop creation tools and powerful developer enhancements including modern vendor packages, reusable components, rich APIs and hooks to modify and extend the editor through Custom Blocks, Custom Block Styles and Plugins. ## Quick links +### Create pages and posts with the block editor + +In the Block Editor Handbook, our tutorials will be development-focussed. However, it helps if you have some experience using the block editor the way an end-user would first. If you have no experience building with the block editor yet, we recommend you [learn to use the block editor](https://wordpress.org/support/article/wordpress-editor/) to create posts and pages. + ### Create a Block Tutorial -[Learn how to create your first block](/docs/getting-started/create-block/README.md) for the WordPress block editor. From setting up your development environment, tools, and getting comfortable with the new development model, this tutorial covers all what you need to know to get started with the block editor. +[Learn how to create your first block](/docs/getting-started/create-block/README.md) for the WordPress block editor. From setting up your development environment, tools, and getting comfortable with the new development model, this tutorial covers all you need to know to get started with creating blocks. ### Develop for the block editor -Whether you want to extend the functionality of the block editor, or create a plugin based on it, [see the developer documentation](/docs/how-to-guides/README.md) to find all the information about the basic concepts you need to get started, the block editor APIs and its architecture. +Whether you want to extend the functionality of the block editor, or create a plugin based on it, [see our how-to guides](/docs/how-to-guides/README.md) to find all the information about the basic concepts you need to get started, the block editor APIs and its architecture. - [Gutenberg Architecture](/docs/explanations/architecture/README.md) - [Block Styles](/docs/reference-guides/block-api/block-styles.md) @@ -38,4 +40,4 @@ Whether you want to extend the functionality of the block editor, or create a pl ### Contribute to the block editor -Everything you need to know to [start contributing to the block editor](/docs/contributors/README.md) . Whether you are interested in the design, code, triage, documentation, support or internationalization of the block editor, you will find here guides to help you. +Everything you need to know to [start contributing to the block editor](/docs/contributors/README.md) . Whether you are interested in the design, code, triage, documentation, support or internationalization of the block editor, you will find guides to help you here. diff --git a/docs/explanations/architecture/README.md b/docs/explanations/architecture/README.md index 723bce3ce4e2f4..774df183618d4a 100644 --- a/docs/explanations/architecture/README.md +++ b/docs/explanations/architecture/README.md @@ -2,12 +2,16 @@ Let’s look at the big picture and the architectural and UX principles of the block editor and the Gutenberg repository. -- [Key Concepts](/docs/explanations/architecture/key-concepts.md) -- [Data Format And Data Flow](/docs/explanations/architecture/data-flow.md) -- [Understand the repository folder structure](/docs/contributors/folder-structure.md). +## Editor + +- [Key concepts](/docs/explanations/architecture/key-concepts.md). +- [Data format and data flow](/docs/explanations/architecture/data-flow.md). +- [Site editing templates](/docs/explanations/architecture/full-site-editing-templates.md). +- [Styles in the editor](/docs/explanations/architecture/styles.md). +- [Performance](/docs/explanations/architecture/performance.md). + +## Gutenberg Repository + - [Modularity and WordPress Packages](/docs/explanations/architecture/modularity.md). -- [Block Editor Performance](/docs/explanations/architecture/performance.md). -- What are the decision decisions behind the Data Module? -- [Why is Puppeteer the tool of choice for end-to-end tests?](/docs/explanations/architecture/automated-testing.md) -- [What's the difference between the different editor packages? What's the purpose of each package?](/docs/explanations/architecture/modularity.md#whats-the-difference-between-the-different-editor-packages-whats-the-purpose-of-each-package) -- [Template and template parts flows](/docs/explanations/architecture/full-site-editing-templates.md) +- [Understand the repository folder structure](/docs/contributors/folder-structure.md). +- **Outdated!** [Why is Puppeteer the tool of choice for end-to-end tests?](/docs/explanations/architecture/automated-testing.md). diff --git a/docs/explanations/architecture/full-site-editing-templates.md b/docs/explanations/architecture/full-site-editing-templates.md index 76bc3a0f5861f2..aa18ad17a18f6f 100644 --- a/docs/explanations/architecture/full-site-editing-templates.md +++ b/docs/explanations/architecture/full-site-editing-templates.md @@ -1,4 +1,4 @@ -# Full Site Editing Templates +# Site Editing Templates ## Template and template part flows diff --git a/docs/explanations/architecture/key-concepts.md b/docs/explanations/architecture/key-concepts.md index b246724e5d5838..3af2d6d87a077d 100644 --- a/docs/explanations/architecture/key-concepts.md +++ b/docs/explanations/architecture/key-concepts.md @@ -26,11 +26,13 @@ Blocks can be static or dynamic. Static blocks contain rendered content and an o Each block contains Attributes or configuration settings, which can be sourced from raw HTML in the content via meta or other customizable origins. -### Transformations +More on [Data format and data flow](/docs/explanations/architecture/data-flow.md). -Blocks have the ability to be transformed into other block types. This allows basic operations like converting a paragraph into a heading, but also more intricate ones like multiple images becoming a gallery. Transformations work for single blocks and for multi-block selections. Internal block variations are also possible transformation targets. +### Block Transforms -### Variations +Blocks have the ability to be transformed into other block types. This allows basic operations like converting a paragraph into a heading, but also more intricate ones like multiple images becoming a gallery. Block transforms work for single blocks and for multi-block selections. Internal block variations are also possible transformation targets. + +### Block Variations Given a block type, a block variation is a predefined set of its initial attributes. This API allows creating a single block from which multiple configurations are possible. Variations provide different possible interfaces, including showing up as entirely new blocks in the library, or as presets when inserting a new block. Read [the API documentation](/docs/reference-guides/block-api/block-registration.md#variations-optional) for more details. @@ -41,24 +43,28 @@ Given a block type, a block variation is a predefined set of its initial attribu ## Reusable Blocks -A reusable blocks is a block (or multiple blocks) that can be inserted and edited globally at once. If a reusable block is edited in one place, those changes are reflected across all posts and pages that block is used. Examples of reusable blocks include a block consisting of a heading whose content and a custom color that would be appear on multiple pages of the site and sidebar widgets that would appear on every page. +A reusable blocks is **an instance** of a block (or multiple blocks) that can be inserted and edited in multiples places, remaining in sync everywhere. If a reusable block is edited in one place, those changes are reflected across all posts and pages that block is used. Examples of reusable blocks include a block consisting of a heading whose content and a custom color that would be appear on multiple pages of the site and sidebar widgets that would appear on every page. Any edits to a reusable block will appear on every other use of that block, saving time from having to make the same edit on different posts. -In technical details, reusable blocks are stored as a hidden post type (`wp_block`) and are dynamic blocks that "ref" or reference the `post_id` and return the `post_content` for that block. +Internally, reusable blocks are stored as a hidden post type (`wp_block`) and are dynamic blocks that "ref" or reference the `post_id` and return the `post_content` for that block. ## Patterns -A [block pattern](/docs/reference-guides/block-api/block-patterns.md) is a group of blocks that have been combined together creating a design pattern. These design patterns provide a starting point for building more advanced pages and layouts quickly. A block pattern can be as small as a single block or as large as a full page of content. Unlike reusable blocks, once a pattern is inserted it doesn't remain in sync with the original content as the blocks contained are meant to be edited and customized by the user. Underneath the surface, patterns are just regular blocks composed together. Themes can register patterns to offer users quick starting points with a design language familiar to that theme's aesthetics. +A [block pattern](/docs/reference-guides/block-api/block-patterns.md) is a group of blocks that have been combined together creating a design pattern. These design patterns provide a starting point for building more advanced pages and layouts quickly, instead of inserting individual blocks. A block pattern can be as small as a single block or as large as a full page of content. Unlike reusable blocks, once a pattern is inserted it doesn't remain in sync with the original content as the blocks contained are meant to be edited and customized by the user. Underneath the surface, patterns are just regular blocks composed together. Themes can register patterns to offer users quick starting points with a design language familiar to that theme's aesthetics. + +## Templates -## Templates (in progress) +While the post editor concentrates on the content of a post, the [template](/docs/reference-guides/block-api/block-templates.md) editor allows declaring and editing an entire site using blocks, from header to footer. Templates are broken down between templates (that describe a full page) and template parts (that describe reusable areas within a template, including semantic areas like header, sidebar, and footer). -While the post editor concentrates on the content of a post, the [template](/docs/reference-guides/block-api/block-templates.md) editor allows declaring and editing an entire site using blocks, from header to footer. To support these efforts there's a collection of blocks that interact with different parts of a site (like the site title, description, logo, navigation, etc) as well as semantic areas like header, sidebar, and footer. Templates are broken down between templates (that describe a full page) and template parts (that describe reusable areas within a template). These templates and template parts can be composed together and registered by a theme. They are also entirely editable by users using the block editor. Customized templates are saved in a `wp_template` post type. Block templates include both static pages and dynamic ones, like archives, singular, home, 404, etc. +These templates and template parts can be composed together and registered by a theme. They are also entirely editable by users using the block editor; a collection of blocks that interact with different properties and settings of the site (like the site title, description, logo, navigation, etc) are especially useful when editing templates and template parts. Customized templates are saved in a `wp_template` post type. Block templates include both static pages and dynamic ones, like archives, singular, home, 404, etc. Note: custom post types can also be initialized with a starting `post_content` template that should not be confused with the theme template system described above. -## Global Styles +More on [Site editing templates](/docs/explanations/architecture/full-site-editing-templates.md). + +## Styles -Global Styles is both an interface (which users access through the site editor) and a configuration system done through [a `theme.json` file](/docs/how-to-guides/themes/theme-json.md). This file absorbs most of the configuration aspects usually scattered through various `add_theme_support` calls to simplify communicating with the editor. It thus aims to improve declaring what settings should be enabled, what specific tools a theme offers (like a custom color palette), the available design tools present, and an infrastructure that allows to coordinate the styles coming from WordPress, the active theme, and the user. +Styles, formerly known as Global Styles and as such referenced in the code, is both an interface that users access through the editor and a configuration system done through [a `theme.json` file](/docs/how-to-guides/themes/theme-json.md). This file absorbs most of the configuration aspects usually scattered through various `add_theme_support` calls to simplify communicating with the editor. It thus aims to improve declaring what settings should be enabled, what specific tools a theme offers (like a custom color palette), the available design tools present, and an infrastructure that allows to coordinate the styles coming from WordPress, the active theme, and the user. Learn more about [Global Styles](/docs/explanations/architecture/styles.md#global-styles). diff --git a/docs/explanations/architecture/styles.md b/docs/explanations/architecture/styles.md index 416eef796bec8f..a1ed2c33054bc9 100644 --- a/docs/explanations/architecture/styles.md +++ b/docs/explanations/architecture/styles.md @@ -1,23 +1,28 @@ -## Styles in the block editor +## Styles in the editor This document introduces the main concepts related to styles that affect the user content in the block editor. It points to the relevant reference guides and tutorials for readers to dig deeper into each one of the ideas presented. It's aimed to block authors and people working in the block editor project. 1. [HTML and CSS](#html-and-css) 2. [Block styles](#block-styles) - - [From UI controls to HTML markup](#from-ui-controls-to-html-markup) - - [Block Supports API](#block-supports-api) - - [Current limitations of the Block Supports API](#current-limitations-of-the-block-supports-api) + +- [From UI controls to HTML markup](#from-ui-controls-to-html-markup) +- [Block Supports API](#block-supports-api) +- [Current limitations of the Block Supports API](#current-limitations-of-the-block-supports-api) + 3. [Global styles](#global-styles) - - [Gather data](#gather-data) - - [Consolidate data](#consolidate-data) - - [From data to styles](#from-data-to-styles) - - [Current limitations of the Global Styles API](#current-limitations-of-the-global-styles-api) + +- [Gather data](#gather-data) +- [Consolidate data](#consolidate-data) +- [From data to styles](#from-data-to-styles) +- [Current limitations of the Global Styles API](#current-limitations-of-the-global-styles-api) + 4. [Layout styles](#layout-styles) - - [Base layout styles](#base-layout-styles) - - [Individual layout styles](#individual-layout-styles) - - [Available layout types](#available-layout-types) - - [Targeting layout or container blocks from themes](#targeting-layout-or-container-blocks-from-themes) - - [Opting out of generated layout styles](#opting-out-of-generated-layout-styles) + +- [Base layout styles](#base-layout-styles) +- [Individual layout styles](#individual-layout-styles) +- [Available layout types](#available-layout-types) +- [Targeting layout or container blocks from themes](#targeting-layout-or-container-blocks-from-themes) +- [Opting out of generated layout styles](#opting-out-of-generated-layout-styles) ### HTML and CSS @@ -524,16 +529,16 @@ Common layout definitions are stored in [the core `theme.json` file](https://git When a block that opts in to the experimental layout support is rendered, two things are processed and added to the output via [`layout.php`](https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/block-supports/layout.php): -- Semantic class names are added to the block markup to indicate which layout settings are in use. For example, `is-layout-flow` is for blocks (such as Group) that use the default/flow layout, and `is-content-justification-right` is added when a user sets a block to use right justification. -- Individual styles are generated for non-default layout values that are set on the individual block being rendered. These styles are attached to the block via a container class name using the form `wp-container-$id` where the `$id` is a [unique number](https://developer.wordpress.org/reference/functions/wp_unique_id/). +- Semantic class names are added to the block markup to indicate which layout settings are in use. For example, `is-layout-flow` is for blocks (such as Group) that use the default/flow layout, and `is-content-justification-right` is added when a user sets a block to use right justification. +- Individual styles are generated for non-default layout values that are set on the individual block being rendered. These styles are attached to the block via a container class name using the form `wp-container-$id` where the `$id` is a [unique number](https://developer.wordpress.org/reference/functions/wp_unique_id/). #### Available layout types There are currently three layout types in use: -* Default/Flow: Items are stacked vertically. The parent container block is set to `display: flow` and the spacing between children is handled via vertical margins. -* Constrained: Items are stacked vertically, using the same spacing logic as the Flow layout. Features constrained widths for child content, outputting widths for standard content size and wide size. Defaults to using global `contentSize` and `wideSize` values set in `settings.layout` in the `theme.json`. -* Flex: Items are displayed using a Flexbox layout. Defaults to a horizontal orientation. Spacing between children is handled via the `gap` CSS property. +- Default/Flow: Items are stacked vertically. The parent container block is set to `display: flow` and the spacing between children is handled via vertical margins. +- Constrained: Items are stacked vertically, using the same spacing logic as the Flow layout. Features constrained widths for child content, outputting widths for standard content size and wide size. Defaults to using global `contentSize` and `wideSize` values set in `settings.layout` in the `theme.json`. +- Flex: Items are displayed using a Flexbox layout. Defaults to a horizontal orientation. Spacing between children is handled via the `gap` CSS property. For controlling spacing between blocks, and enabling block spacing controls see: [What is blockGap and how can I use it?](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-json/#what-is-blockgap-and-how-can-i-use-it). @@ -551,17 +556,17 @@ Work is currently underway to expand stable semantic classnames in Layout block The current semantic class names that can be output by the Layout block support are: -* `is-layout-flow`: Blocks that use the Default/Flow layout type. -* `is-layout-constrained`: Blocks that use the Constrained layout type. -* `is-layout-flex`: Blocks that use the Flex layout type. -* `wp-container-$id`: Where `$id` is a semi-random number. A container class that only exists when the block contains non-default Layout values. This class should not be used directly for any CSS targeting as it may or may not be present. -* `is-horizontal`: When a block explicitly sets `orientation` to `horizontal`. -* `is-vertical`: When a block explicitly sets `orientation` to `vertical`. -* `is-content-justification-left`: When a block explicitly sets `justifyContent` to `left`. -* `is-content-justification-center`: When a block explicitly sets `justifyContent` to `center`. -* `is-content-justification-right`: When a block explicitly sets `justifyContent` to `right`. -* `is-content-justification-space-between`: When a block explicitly sets `justifyContent` to `space-between`. -* `is-nowrap`: When a block explicitly sets `flexWrap` to `nowrap`. +- `is-layout-flow`: Blocks that use the Default/Flow layout type. +- `is-layout-constrained`: Blocks that use the Constrained layout type. +- `is-layout-flex`: Blocks that use the Flex layout type. +- `wp-container-$id`: Where `$id` is a semi-random number. A container class that only exists when the block contains non-default Layout values. This class should not be used directly for any CSS targeting as it may or may not be present. +- `is-horizontal`: When a block explicitly sets `orientation` to `horizontal`. +- `is-vertical`: When a block explicitly sets `orientation` to `vertical`. +- `is-content-justification-left`: When a block explicitly sets `justifyContent` to `left`. +- `is-content-justification-center`: When a block explicitly sets `justifyContent` to `center`. +- `is-content-justification-right`: When a block explicitly sets `justifyContent` to `right`. +- `is-content-justification-space-between`: When a block explicitly sets `justifyContent` to `space-between`. +- `is-nowrap`: When a block explicitly sets `flexWrap` to `nowrap`. #### Opting out of generated layout styles diff --git a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md index 2aa91cd1a067e9..282578fbb66dce 100644 --- a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md +++ b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md @@ -247,7 +247,7 @@ Voila! We can now filter the results: Let’s take a pause for a moment to consider the downsides of an alternative approach we could have taken - working with the API directly. Imagine we sent the API requests directly: ```js -import { apiFetch } from '@wordpress/api-fetch'; +import apiFetch from '@wordpress/api-fetch'; function MyFirstApp() { // ... const [pages, setPages] = useState( [] ); diff --git a/docs/how-to-guides/javascript/versions-and-building.md b/docs/how-to-guides/javascript/versions-and-building.md index 257b73c6866f16..ec9f98368ff024 100644 --- a/docs/how-to-guides/javascript/versions-and-building.md +++ b/docs/how-to-guides/javascript/versions-and-building.md @@ -4,8 +4,7 @@ The Block Editor Handbook shows JavaScript examples in two syntaxes: JSX and Pla Plain refers to JavaScript code compatible with WordPress's minimum [target for browser support](https://make.wordpress.org/core/handbook/best-practices/browser-support/) without requiring a transpilation step. This step is commonly referred to as a build process. -"JSX" doesn't refer to a specific version of JavaScript, but refers to the latest language definition plus -[JSX syntax](https://reactjs.org/docs/introducing-jsx.html), a syntax that blends HTML and JavaScript. JSX makes it easier to read and write markup code, but does require a build step to transpile into code compatible with browsers. Webpack and babel are the tools that perform this transpilation step. +"JSX" doesn't refer to a specific version of JavaScript, but refers to the latest language definition plus [JSX syntax](https://reactjs.org/docs/introducing-jsx.html), a syntax that blends HTML and JavaScript. JSX makes it easier to read and write markup code, but does require a build step to transpile into code compatible with browsers. Webpack and babel are the tools that perform this transpilation step. For simplicity, the JavaScript tutorial uses the Plain definition, without JSX. This code can run straight in your browser and does not require an additional build step. In many cases, it is perfectly fine to follow the same approach for simple plugins or experimenting. As your codebase grows in complexity it might be a good idea to switch to JSX. You will find the majority of code and documentation across the block editor uses JSX. diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md index 8833b02d3b1244..efd4820b1adf55 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/theme-json.md @@ -1067,7 +1067,7 @@ h3 { ##### Element pseudo selectors -Pseudo selectors `:hover`, `:focus`, `:visited` are supported by Gutenberg. +Pseudo selectors `:hover`, `:focus`, `:visited`, `:active`, `:link`, `:any-link` are supported by Gutenberg. ```json "elements": { diff --git a/docs/manifest.json b/docs/manifest.json index 65e3244c544b49..b1dfb002ac3141 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2028,7 +2028,7 @@ "parent": "architecture" }, { - "title": "Full Site Editing Templates", + "title": "Site Editing Templates", "slug": "full-site-editing-templates", "markdown_source": "../docs/explanations/architecture/full-site-editing-templates.md", "parent": "architecture" diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index d78050d79a9a1e..63219182dd1c2c 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -28,6 +28,9 @@ Starting in WordPress 5.8 release, we encourage using the `block.json` metadata "my-plugin/message": "message" }, "usesContext": [ "groupId" ], + "selectors": { + "root": ".wp-block-my-plugin-notice" + }, "supports": { "align": true }, @@ -379,6 +382,39 @@ See [the block context documentation](/docs/reference-guides/block-api/block-con } ``` +### Selectors + +- Type: `object` +- Optional +- Localized: No +- Property: `selectors` +- Default: `{}` +- Since: `WordPress 6.3.0` + +Any custom CSS selectors, keyed by `root`, feature, or sub-feature, to be used +when generating block styles for theme.json (global styles) stylesheets. +Providing custom selectors allows more fine grained control over which styles +apply to what block elements, e.g. applying typography styles only to an inner +heading while colors are still applied on the outer block wrapper etc. + + +See the [the selectors documentation](/docs/reference-guides/block-api/block-selectors.md) for more details. + +```json +{ + "selectors": { + "root": ".my-custom-block-selector", + "color": { + "text": ".my-custom-block-selector p" + }, + "typography": { + "root": ".my-custom-block-selector > h2", + "text-decoration": ".my-custom-block-selector > h2 span" + } + } +} +``` + ### Supports - Type: `object` diff --git a/docs/reference-guides/block-api/block-selectors.md b/docs/reference-guides/block-api/block-selectors.md new file mode 100644 index 00000000000000..9ce605782c628e --- /dev/null +++ b/docs/reference-guides/block-api/block-selectors.md @@ -0,0 +1,116 @@ +# Selectors + +Block Selectors is the API that allows blocks to customize the CSS selector used +when their styles are generated. + +A block may customize its CSS selectors at three levels: root, feature, and +subfeature. + +## Root Selector + +The root selector is the block's primary CSS selector. + +All blocks require a primary CSS selector for their style declarations to be +included under. If one is not provided through the Block Selectors API, a +default is generated in the form of `.wp-block-`. + +### Example +```json +{ + ... + "selectors": { + "root": ".my-custom-block-selector" + } +} +``` + +## Feature Selectors + +Feature selectors relate to styles for a block support, e.g. border, color, +typography, etc. + +A block may wish to apply the styles for specific features to different +elements within a block. An example might be using colors on the block's wrapper +but applying the typography styles to an inner heading only. + +### Example +```json +{ + ... + "selectors": { + "root": ".my-custom-block-selector", + "color": ".my-custom-block-selector", + "typography": ".my-custom-block-selector > h2" + } +} +``` + +## Subfeature Selectors + +These selectors relate to individual styles provided by a block support e.g. +`background-color` + +A subfeature can have styles generated under its own unique selector. This is +especially useful where one block support subfeature can't be applied to the +same element as the support's other subfeatures. + +A great example of this is `text-decoration`. Web browsers render this style +differently, making it difficult to override if added to a wrapper element. By +assigning `text-decoration` a custom selector, its style can target only the +elements to which it should be applied. + +### Example +```json +{ + ... + "selectors": { + "root": ".my-custom-block-selector", + "color": ".my-custom-block-selector", + "typography": { + "root": ".my-custom-block-selector > h2", + "text-decoration": ".my-custom-block-selector > h2 span" + } + } +} +``` + +## Shorthand + +Rather than specify a CSS selector for every subfeature, you can set a single +selector as a string value for the relevant feature. This is the approach +demonstrated for the `color` feature in the earlier examples above. + +## Fallbacks + +A selector that hasn't been configured for a specific feature will fall back to +the block's root selector. Similarly, if a subfeature hasn't had a custom +selector set, it will fall back to its parent feature's selector and, if unavailable, fall back further to the block's root selector. + +Rather than repeating selectors for multiple subfeatures, you can set the +common selector as the parent feature's `root` selector and only define the +unique selectors for the subfeatures that differ. + +### Example +```json +{ + ... + "selectors": { + "root": ".my-custom-block-selector", + "color": { + "text": ".my-custom-block-selector p" + }, + "typography": { + "root": ".my-custom-block-selector > h2", + "text-decoration": ".my-custom-block-selector > h2 span" + } + } +} +``` + +The `color.background-color` subfeature isn't explicitly set in the above +example. As the `color` feature also doesn't define a `root` selector, +`color.background-color` would be included under the block's primary root +selector, `.my-custom-block-selector`. + +For a subfeature such as `typography.font-size`, it would fallback to its parent +feature's selector given that is present, i.e. `.my-custom-block-selector > h2`. \ No newline at end of file diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 1dee5a7509d7b1..e5b524c6850860 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -59,7 +59,7 @@ Prompt visitors to take action with a group of button-style links. ([Source](htt - **Name:** core/buttons - **Category:** design -- **Supports:** align (full, wide), anchor, spacing (blockGap, margin), typography (fontSize, lineHeight) +- **Supports:** align (full, wide), anchor, spacing (blockGap, margin), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** ## Calendar @@ -105,7 +105,7 @@ Display content in multiple columns, with blocks added to each column. ([Source] - **Name:** core/columns - **Category:** design - **Supports:** align (full, wide), anchor, color (background, gradients, link, text), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** isStackedOnMobile, verticalAlignment +- **Attributes:** isStackedOnMobile, templateLock, verticalAlignment ## Comment Author Avatar (deprecated) @@ -572,7 +572,7 @@ Contains the block elements used to render a post, like the title, date, feature - **Name:** core/post-template - **Category:** theme -- **Supports:** align, anchor, color (background, gradients, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** align (full, wide), anchor, color (background, gradients, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** ## Post Terms @@ -590,7 +590,7 @@ Show minutes required to finish reading the post. ([Source](https://github.com/W - **Name:** core/post-time-to-read - **Category:** theme -- **Supports:** ~~html~~, ~~multiple~~ +- **Supports:** spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign ## Post Title @@ -689,7 +689,7 @@ Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Ju - **Name:** core/quote - **Category:** text -- **Supports:** anchor, color (background, gradients, link, text), typography (fontSize, lineHeight) +- **Supports:** anchor, color (background, gradients, link, text), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** align, citation, value ## Read More diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index c7bb892103e004..2342fc0656221f 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -60,6 +60,7 @@ Settings related to shadows. | Property | Type | Default | Props | | --- | --- | --- |--- | +| defaultPresets | boolean | true | | | presets | array | | name, shadow, slug | --- diff --git a/gutenberg.php b/gutenberg.php index ce3d4b810f83a5..9b5fbc800dd639 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.0 * Requires PHP: 5.6 - * Version: 15.4.0-rc.1 + * Version: 15.5.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/duotone.php b/lib/block-supports/duotone.php index ef950f80ce9448..1884283c3b150a 100644 --- a/lib/block-supports/duotone.php +++ b/lib/block-supports/duotone.php @@ -426,128 +426,12 @@ function gutenberg_register_duotone_support( $block_type ) { * * @param string $block_content Rendered block content. * @param array $block Block object. + * @deprecated 6.3.0 Use WP_Duotone_Gutenberg::render_duotone_support() instead. * @return string Filtered block content. */ function gutenberg_render_duotone_support( $block_content, $block ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - - $duotone_support = false; - if ( $block_type && property_exists( $block_type, 'supports' ) ) { - $duotone_support = _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), false ); - } - - $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] ); - - if ( - ! $duotone_support || - ! $has_duotone_attribute - ) { - return $block_content; - } - - // Possible values for duotone attribute: - // 1. Array of colors - e.g. array('#000000', '#ffffff'). - // 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|green-blue'. - // 3. A CSS string - e.g. 'unset' to remove globally applied duotone. - $duotone_attr = $block['attrs']['style']['color']['duotone']; - - $is_preset = is_string( $duotone_attr ) && strpos( $duotone_attr, 'var:preset|duotone|' ) === 0; - $is_css = is_string( $duotone_attr ) && strpos( $duotone_attr, 'var:preset|duotone|' ) === false; - $is_custom = is_array( $duotone_attr ); - - // Generate the pieces needed for rendering a duotone to the page. - if ( $is_preset ) { - // Extract the slug from the preset variable string. - $slug = str_replace( 'var:preset|duotone|', '', $duotone_attr ); - - // Utilize existing preset CSS custom property. - $filter_property = "var(--wp--preset--duotone--$slug)"; - } elseif ( $is_css ) { - // Build a unique slug for the filter based on the CSS value. - $slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) ); - - // Pass through the CSS value. - $filter_property = $duotone_attr; - } elseif ( $is_custom ) { - // Build a unique slug for the filter based on the array of colors. - $slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) ); - - // This has the same shape as a preset, so it can be used in place of a - // preset when getting the filter property and SVG filter. - $filter_data = array( - 'slug' => $slug, - 'colors' => $duotone_attr, - ); - - // Build a customized CSS filter property for unique slug. - $filter_property = gutenberg_get_duotone_filter_property( $filter_data ); - - // SVG will be output on the page later. - $filter_svg = gutenberg_get_duotone_filter_svg( $filter_data ); - } - - // - Applied as a class attribute to the block wrapper. - // - Used as a selector to apply the filter to the block. - $filter_id = gutenberg_get_duotone_filter_id( array( 'slug' => $slug ) ); - - // Build the CSS selectors to which the filter will be applied. - $selector = WP_Theme_JSON_Gutenberg::scope_selector( '.' . $filter_id, $duotone_support ); - - // Calling gutenberg_style_engine_get_stylesheet_from_css_rules ensures that - // the styles are rendered in an inline for block supports because we're - // using the `context` option to instruct it so. - gutenberg_style_engine_get_stylesheet_from_css_rules( - array( - array( - 'selector' => $selector, - 'declarations' => array( - // !important is needed because these styles - // render before global styles, - // and they should be overriding the duotone - // filters set by global styles. - 'filter' => $filter_property . ' !important', - ), - ), - ), - array( - 'context' => 'block-supports', - ) - ); - - // If we needed to generate an SVG, output it on the page. - if ( isset( $filter_svg ) ) { - add_action( - 'wp_footer', - static function () use ( $filter_svg, $selector ) { - echo $filter_svg; - - /* - * Safari renders elements incorrectly on first paint when the - * SVG filter comes after the content that it is filtering, so - * we force a repaint with a WebKit hack which solves the issue. - */ - global $is_safari; - if ( $is_safari ) { - /* - * Simply accessing el.offsetHeight flushes layout and style - * changes in WebKit without having to wait for setTimeout. - */ - printf( - '', - wp_json_encode( $selector ) - ); - } - } - ); - } - - // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper. - return preg_replace( - '/' . preg_quote( 'class="', '/' ) . '/', - 'class="' . $filter_id . ' ', - $block_content, - 1 - ); + _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Duotone_Gutenberg::render_duotone_support' ); + return WP_Duotone_Gutenberg::render_duotone_support( $block_content, $block ); } // Register the block support. @@ -558,6 +442,11 @@ static function () use ( $filter_svg, $selector ) { ) ); +add_action( 'wp_loaded', array( 'WP_Duotone_Gutenberg', 'set_global_styles_presets' ), 10 ); +add_action( 'wp_loaded', array( 'WP_Duotone_Gutenberg', 'set_global_style_block_names' ), 10 ); // Remove WordPress core filter to avoid rendering duplicate support elements. remove_filter( 'render_block', 'wp_render_duotone_support', 10, 2 ); -add_filter( 'render_block', 'gutenberg_render_duotone_support', 10, 2 ); +add_filter( 'render_block', array( 'WP_Duotone_Gutenberg', 'render_duotone_support' ), 10, 2 ); +add_action( 'wp_enqueue_scripts', array( 'WP_Duotone_Gutenberg', 'output_global_styles' ), 11 ); +add_action( 'wp_footer', array( 'WP_Duotone_Gutenberg', 'output_footer_assets' ), 10 ); +add_filter( 'block_editor_settings_all', array( 'WP_Duotone_Gutenberg', 'add_editor_settings' ), 10 ); diff --git a/lib/block-supports/elements.php b/lib/block-supports/elements.php index 5391478010964a..d4502aaa2e4785 100644 --- a/lib/block-supports/elements.php +++ b/lib/block-supports/elements.php @@ -39,13 +39,18 @@ function gutenberg_render_elements_support( $block_content, $block ) { $link_color = _wp_array_get( $block['attrs'], array( 'style', 'elements', 'link', 'color', 'text' ), null ); } + $hover_link_color = null; + if ( ! empty( $block['attrs'] ) ) { + $hover_link_color = _wp_array_get( $block['attrs'], array( 'style', 'elements', 'link', ':hover', 'color', 'text' ), null ); + } + /* - * For now we only care about link color. + * For now we only care about link colors. * This code in the future when we have a public API * should take advantage of WP_Theme_JSON_Gutenberg::compute_style_properties * and work for any element and style. */ - if ( null === $link_color ) { + if ( null === $link_color && null === $hover_link_color ) { return $block_content; } @@ -94,6 +99,16 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) { ) ); + if ( isset( $link_block_styles[':hover'] ) ) { + gutenberg_style_engine_get_styles( + $link_block_styles[':hover'], + array( + 'selector' => ".$class_name a:hover", + 'context' => 'block-supports', + ) + ); + } + return null; } diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 70029166ce658c..33f616ac7ee8d2 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -280,6 +280,37 @@ function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support ); } } + } elseif ( 'grid' === $layout_type ) { + $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem'; + + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))' ), + ); + + if ( $has_block_gap_support && isset( $gap_value ) ) { + $combined_gap_value = ''; + $gap_sides = is_array( $gap_value ) ? array( 'top', 'left' ) : array( 'top' ); + + foreach ( $gap_sides as $gap_side ) { + $process_value = is_string( $gap_value ) ? $gap_value : _wp_array_get( $gap_value, array( $gap_side ), $fallback_gap_value ); + // Get spacing CSS variable from preset value if provided. + if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) { + $index_to_splice = strrpos( $process_value, '|' ) + 1; + $slug = _wp_to_kebab_case( substr( $process_value, $index_to_splice ) ); + $process_value = "var(--wp--preset--spacing--$slug)"; + } + $combined_gap_value .= "$process_value "; + } + $gap_value = trim( $combined_gap_value ); + + if ( null !== $gap_value && ! $should_skip_gap_serialization ) { + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'gap' => $gap_value ), + ); + } + } } if ( ! empty( $layout_styles ) ) { @@ -549,7 +580,7 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) { if ( wp_theme_has_theme_json() || 1 === preg_match( $group_with_inner_container_regex, $block_content ) || - ( isset( $block['attrs']['layout']['type'] ) && 'flex' === $block['attrs']['layout']['type'] ) + ( isset( $block['attrs']['layout']['type'] ) && ( 'flex' === $block['attrs']['layout']['type'] || 'grid' === $block['attrs']['layout']['type'] ) ) ) { return $block_content; } diff --git a/lib/class-wp-duotone-gutenberg.php b/lib/class-wp-duotone-gutenberg.php new file mode 100644 index 00000000000000..ff963f46cb4555 --- /dev/null +++ b/lib/class-wp-duotone-gutenberg.php @@ -0,0 +1,389 @@ + + * [ + * 'slug' => 'blue-orange', + * 'colors' => [ '#0000ff', '#ffcc00' ], + * ] + * ], + * … + * ] + * + * @since 6.3.0 + * @var array + */ + private static $global_styles_presets = array(); + + /** + * An array of block names from global, theme, and custom styles that have duotone presets. We'll use this to quickly + * check if a block being rendered needs to have duotone applied, and which duotone preset to use. + * + * Example: + * [ + * 'core/featured-image' => 'blue-orange', + * … + * ] + * + * @since 6.3.0 + * @var array + */ + private static $global_styles_block_names = array(); + + /** + * An array of Duotone SVG and CSS output needed for the frontend duotone rendering based on what is + * being output on the page. Organized by a slug of the preset/color group and the information needed + * to generate the SVG and CSS at render. + * + * Example: + * [ + * 'blue-orange' => [ + * 'slug' => 'blue-orange', + * 'colors' => [ '#0000ff', '#ffcc00' ], + * ], + * 'wp-duotone-000000-ffffff-2' => [ + * 'slug' => 'wp-duotone-000000-ffffff-2', + * 'colors' => [ '#000000', '#ffffff' ], + * ], + * ] + * + * @since 6.3.0 + * @var array + */ + private static $output = array(); + + /** + * Prefix used for generating and referencing duotone CSS custom properties. + */ + const CSS_VAR_PREFIX = '--wp--preset--duotone--'; + + /** + * Get all possible duotone presets from global and theme styles and store as slug => [ colors array ] + * We only want to process this one time. On block render we'll access and output only the needed presets for that page. + */ + public static function set_global_styles_presets() { + // Get the per block settings from the theme.json. + $tree = gutenberg_get_global_settings(); + $presets_by_origin = _wp_array_get( $tree, array( 'color', 'duotone' ), array() ); + + foreach ( $presets_by_origin as $presets ) { + foreach ( $presets as $preset ) { + self::$global_styles_presets[ _wp_to_kebab_case( $preset['slug'] ) ] = array( + 'slug' => $preset['slug'], + 'colors' => $preset['colors'], + ); + } + } + } + + /** + * Scrape all block names from global styles and store in self::$global_styles_block_names + */ + public static function set_global_style_block_names() { + // Get the per block settings from the theme.json. + $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data(); + $block_nodes = $tree->get_styles_block_nodes(); + $theme_json = $tree->get_raw_data(); + + foreach ( $block_nodes as $block_node ) { + // This block definition doesn't include any duotone settings. Skip it. + if ( empty( $block_node['duotone'] ) ) { + continue; + } + + // Value looks like this: 'var(--wp--preset--duotone--blue-orange)' or 'var:preset|duotone|default-filter'. + $duotone_attr_path = array_merge( $block_node['path'], array( 'filter', 'duotone' ) ); + $duotone_attr = _wp_array_get( $theme_json, $duotone_attr_path, array() ); + + if ( empty( $duotone_attr ) ) { + continue; + } + // If it has a duotone filter preset, save the block name and the preset slug. + $slug = self::gutenberg_get_slug_from_attr( $duotone_attr ); + + if ( $slug && $slug !== $duotone_attr ) { + self::$global_styles_block_names[ $block_node['name'] ] = $slug; + } + } + } + + /** + * Take the inline CSS duotone variable from a block and return the slug. Handles styles slugs like: + * var:preset|duotone|default-filter + * var(--wp--preset--duotone--blue-orange) + * + * @param string $duotone_attr The duotone attribute from a block. + * @return string The slug of the duotone preset or an empty string if no slug is found. + */ + private static function gutenberg_get_slug_from_attr( $duotone_attr ) { + // Uses Branch Reset Groups `(?|…)` to return one capture group. + preg_match( '/(?|var:preset\|duotone\|(\S+)|var\(--wp--preset--duotone--(\S+)\))/', $duotone_attr, $matches ); + + return ! empty( $matches[1] ) ? $matches[1] : ''; + } + + /** + * Check if we have a valid duotone preset. + * + * @param string $duotone_attr The duotone attribute from a block. + * @return bool True if the duotone preset present and valid. + */ + private static function is_preset( $duotone_attr ) { + $slug = self::gutenberg_get_slug_from_attr( $duotone_attr ); + + return array_key_exists( $slug, self::$global_styles_presets ); + } + + /** + * Get the CSS variable name for a duotone preset. + * + * @param string $slug The slug of the duotone preset. + * @return string The CSS variable name. + */ + private static function get_css_custom_property_name( $slug ) { + return self::CSS_VAR_PREFIX . $slug; + } + + /** + * Get the CSS variable for a duotone preset. + * + * @param string $slug The slug of the duotone preset. + * @return string The CSS variable. + */ + private static function get_css_var( $slug ) { + return 'var(' . self::get_css_custom_property_name( $slug ) . ')'; + } + + /** + * Get the CSS declaration for a duotone preset. + * Example: --wp--preset--duotone--blue-orange: url('#wp-duotone-blue-orange'); + * + * @param array $filter_data The duotone data for presets and custom filters. + * @return string The CSS declaration. + */ + private static function get_css_custom_property_declaration( $filter_data ) { + $declaration_value = gutenberg_get_duotone_filter_property( $filter_data ); + $duotone_preset_css_property_name = self::get_css_custom_property_name( $filter_data['slug'] ); + return $duotone_preset_css_property_name . ': ' . $declaration_value . ';'; + } + + /** + * Outputs all necessary SVG for duotone filters, CSS for classic themes. + */ + public static function output_footer_assets() { + foreach ( self::$output as $filter_data ) { + + // SVG will be output on the page later. + $filter_svg = gutenberg_get_duotone_filter_svg( $filter_data ); + + echo $filter_svg; + + // This is for classic themes - in block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action. + if ( ! wp_is_block_theme() ) { + wp_add_inline_style( 'core-block-supports', 'body{' . self::get_css_custom_property_declaration( $filter_data ) . '}' ); + } + } + } + + /** + * Adds the duotone SVGs and CSS custom properties to the editor settings so + * they can be pulled in by the EditorStyles component in JS and rendered in + * the post editor. + * + * @param array $settings The block editor settings from the `block_editor_settings_all` filter. + * @return array The editor settings with duotone SVGs and CSS custom properties. + */ + public static function add_editor_settings( $settings ) { + $duotone_svgs = ''; + $duotone_css = 'body{'; + foreach ( self::$global_styles_presets as $filter_data ) { + $duotone_svgs .= gutenberg_get_duotone_filter_svg( $filter_data ); + $duotone_css .= self::get_css_custom_property_declaration( $filter_data ); + } + $duotone_css .= '}'; + + if ( ! isset( $settings['styles'] ) ) { + $settings['styles'] = array(); + } + + $settings['styles'][] = array( + 'assets' => $duotone_svgs, + // The 'svgs' type is new in 6.3 and requires the corresponding JS changes in the EditorStyles component to work. + '__unstableType' => 'svgs', + 'isGlobalStyles' => false, + ); + + $settings['styles'][] = array( + 'css' => $duotone_css, + // This must be set and must be something other than 'theme' or they will be stripped out in the post editor component. + '__unstableType' => 'presets', + // These styles are no longer generated by global styles, so this must be false or they will be stripped out in gutenberg_get_block_editor_settings. + 'isGlobalStyles' => false, + ); + + return $settings; + } + + /** + * Appends the used global style duotone filter CSS Vars to the inline global styles CSS + */ + public static function output_global_styles() { + + if ( empty( self::$output ) ) { + return; + } + + $duotone_css_vars = ''; + + foreach ( self::$output as $filter_data ) { + if ( ! array_key_exists( $filter_data['slug'], self::$global_styles_presets ) ) { + continue; + } + + $duotone_css_vars .= self::get_css_custom_property_declaration( $filter_data ); + } + + if ( ! empty( $duotone_css_vars ) ) { + wp_add_inline_style( 'global-styles', 'body{' . $duotone_css_vars . '}' ); + } + } + + /** + * Render out the duotone CSS styles and SVG. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ + public static function render_duotone_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + + $duotone_support = false; + $duotone_selector = null; + if ( $block_type ) { + $duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' ); + $duotone_support = (bool) $duotone_selector; + } + + // The block should have a duotone attribute or have duotone defined in its theme.json to be processed. + $has_duotone_attribute = isset( $block['attrs']['style']['color']['duotone'] ); + $has_global_styles_duotone = array_key_exists( $block['blockName'], self::$global_styles_block_names ); + + if ( + empty( $block_content ) || + ! $duotone_support || + ( ! $has_duotone_attribute && ! $has_global_styles_duotone ) + ) { + return $block_content; + } + + // Generate the pieces needed for rendering a duotone to the page. + if ( $has_duotone_attribute ) { + + // Possible values for duotone attribute: + // 1. Array of colors - e.g. array('#000000', '#ffffff'). + // 2. Variable for an existing Duotone preset - e.g. 'var:preset|duotone|green-blue' or 'var(--wp--preset--duotone--green-blue)'' + // 3. A CSS string - e.g. 'unset' to remove globally applied duotone. + + $duotone_attr = $block['attrs']['style']['color']['duotone']; + $is_preset = is_string( $duotone_attr ) && self::is_preset( $duotone_attr ); + $is_css = is_string( $duotone_attr ) && ! $is_preset; + $is_custom = is_array( $duotone_attr ); + + if ( $is_preset ) { + + // Extract the slug from the preset variable string. + $slug = self::gutenberg_get_slug_from_attr( $duotone_attr ); + + // Utilize existing preset CSS custom property. + $declaration_value = self::get_css_var( $slug ); + + self::$output[ $slug ] = self::$global_styles_presets[ $slug ]; + + } elseif ( $is_css ) { + // Build a unique slug for the filter based on the CSS value. + $slug = wp_unique_id( sanitize_key( $duotone_attr . '-' ) ); + + // Pass through the CSS value. + $declaration_value = $duotone_attr; + } elseif ( $is_custom ) { + // Build a unique slug for the filter based on the array of colors. + $slug = wp_unique_id( sanitize_key( implode( '-', $duotone_attr ) . '-' ) ); + + $filter_data = array( + 'slug' => $slug, + 'colors' => $duotone_attr, + ); + // Build a customized CSS filter property for unique slug. + $declaration_value = gutenberg_get_duotone_filter_property( $filter_data ); + + self::$output[ $slug ] = $filter_data; + } + } elseif ( $has_global_styles_duotone ) { + $slug = self::$global_styles_block_names[ $block['blockName'] ]; + + // Utilize existing preset CSS custom property. + $declaration_value = self::get_css_var( $slug ); + + self::$output[ $slug ] = self::$global_styles_presets[ $slug ]; + } + + // - Applied as a class attribute to the block wrapper. + // - Used as a selector to apply the filter to the block. + $filter_id = gutenberg_get_duotone_filter_id( array( 'slug' => $slug ) ); + + // Build the CSS selectors to which the filter will be applied. + $selector = WP_Theme_JSON_Gutenberg::scope_selector( '.' . $filter_id, $duotone_selector ); + + // We only want to add the selector if we have it in the output already, essentially skipping 'unset'. + if ( array_key_exists( $slug, self::$output ) ) { + self::$output[ $slug ]['selector'] = $selector; + } + + // Pass styles to the block-supports stylesheet via the style engine. + // This ensures that Duotone styles are included in a single stylesheet, + // avoiding multiple style tags or multiple stylesheets being output to + // the site frontend. + gutenberg_style_engine_get_stylesheet_from_css_rules( + array( + array( + 'selector' => $selector, + 'declarations' => array( + // !important is needed because these styles + // render before global styles, + // and they should be overriding the duotone + // filters set by global styles. + 'filter' => $declaration_value . ' !important', + ), + ), + ), + array( + 'context' => 'block-supports', + ) + ); + + // Like the layout hook, this assumes the hook only applies to blocks with a single wrapper. + $tags = new WP_HTML_Tag_Processor( $block_content ); + if ( $tags->next_tag() ) { + $tags->add_class( $filter_id ); + } + + return $tags->get_updated_html(); + } +} diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 673e97d2b84e5c..c7e1307f4a0bf0 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -81,8 +81,8 @@ class WP_Theme_JSON_Gutenberg { * - prevent_override => Disables override of default presets by theme presets. * The relationship between whether to override the defaults * and whether the defaults are enabled is inverse: - * - If defaults are enabled => theme presets should not be overriden - * - If defaults are disabled => theme presets should be overriden + * - If defaults are enabled => theme presets should not be overridden + * - If defaults are disabled => theme presets should be overridden * For example, a theme sets defaultPalette to false, * making the default palette hidden from the user. * In that case, we want all the theme presets to be present, @@ -144,8 +144,8 @@ class WP_Theme_JSON_Gutenberg { 'path' => array( 'color', 'duotone' ), 'prevent_override' => array( 'color', 'defaultDuotone' ), 'use_default_names' => false, - 'value_func' => 'gutenberg_get_duotone_filter_property', - 'css_vars' => '--wp--preset--duotone--$slug', + 'value_func' => null, // CSS Custom Properties for duotone are handled by block supports in class-wp-duotone-gutenberg.php. + 'css_vars' => null, 'classes' => array(), 'properties' => array( 'filter' ), ), @@ -358,6 +358,9 @@ class WP_Theme_JSON_Gutenberg { 'duotone' => null, 'gradients' => null, 'link' => null, + 'heading' => null, + 'button' => null, + 'caption' => null, 'palette' => null, 'text' => null, ), @@ -559,6 +562,9 @@ public static function get_element_class_name( $element ) { array( 'border', 'style' ), array( 'border', 'width' ), array( 'color', 'link' ), + array( 'color', 'heading' ), + array( 'color', 'button' ), + array( 'color', 'caption' ), array( 'dimensions', 'minHeight' ), // BEGIN EXPERIMENTAL. // Allow `position.fixed` to be opted-in by default. @@ -853,56 +859,22 @@ protected static function get_blocks_metadata() { } foreach ( $blocks as $block_name => $block_type ) { - if ( - isset( $block_type->supports['__experimentalSelector'] ) && - is_string( $block_type->supports['__experimentalSelector'] ) - ) { - static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector']; - } else { - static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ); - } + $root_selector = wp_get_block_css_selector( $block_type ); - if ( - isset( $block_type->supports['color']['__experimentalDuotone'] ) && - is_string( $block_type->supports['color']['__experimentalDuotone'] ) - ) { - static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone']; - } + static::$blocks_metadata[ $block_name ]['selector'] = $root_selector; + static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector ); - // Generate block support feature level selectors if opted into - // for the current block. - $features = array(); - foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) { - if ( - isset( $block_type->supports[ $key ]['__experimentalSelector'] ) && - $block_type->supports[ $key ]['__experimentalSelector'] - ) { - $features[ $feature ] = static::scope_selector( - static::$blocks_metadata[ $block_name ]['selector'], - $block_type->supports[ $key ]['__experimentalSelector'] - ); - } + $elements = static::get_block_element_selectors( $root_selector ); + if ( ! empty( $elements ) ) { + static::$blocks_metadata[ $block_name ]['elements'] = $elements; } - if ( ! empty( $features ) ) { - static::$blocks_metadata[ $block_name ]['features'] = $features; + // The block may or may not have a duotone selector. + $duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' ); + if ( null !== $duotone_selector ) { + static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector; } - // Assign defaults, then overwrite those that the block sets by itself. - // If the block selector is compounded, will append the element to each - // individual block selector. - $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] ); - foreach ( static::ELEMENTS as $el_name => $el_selector ) { - $element_selector = array(); - foreach ( $block_selectors as $selector ) { - if ( $selector === $el_selector ) { - $element_selector = array( $el_selector ); - break; - } - $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' ); - } - static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); - } // If the block has style variations, append their selectors to the block metadata. if ( ! empty( $block_type->styles ) ) { $style_selectors = array(); @@ -1342,7 +1314,7 @@ protected function get_layout_styles( $block_metadata ) { if ( ! empty( $class_name ) && - ! empty( $base_style_rules ) + is_array( $base_style_rules ) ) { // Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`. if ( @@ -2232,8 +2204,8 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { } $feature_selectors = null; - if ( isset( $selectors[ $name ]['features'] ) ) { - $feature_selectors = $selectors[ $name ]['features']; + if ( isset( $selectors[ $name ]['selectors'] ) ) { + $feature_selectors = $selectors[ $name ]['selectors']; } $variation_selectors = array(); @@ -2250,8 +2222,8 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { 'name' => $name, 'path' => array( 'styles', 'blocks', $name ), 'selector' => $selector, + 'selectors' => $feature_selectors, 'duotone' => $duotone_selector, - 'features' => $feature_selectors, 'variations' => $variation_selectors, ); @@ -2297,90 +2269,39 @@ public function get_styles_for_block( $block_metadata ) { $selector = $block_metadata['selector']; $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); - /* - * Process style declarations for block support features the current - * block contains selectors for. Values for a feature with a custom - * selector are filtered from the theme.json node before it is - * processed as normal. - */ - $feature_declarations = array(); - - if ( ! empty( $block_metadata['features'] ) ) { - foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { - if ( ! empty( $node[ $feature_name ] ) ) { - // Create temporary node containing only the feature data - // to leverage existing `compute_style_properties` function. - $feature = array( $feature_name => $node[ $feature_name ] ); - // Generate the feature's declarations only. - $new_feature_declarations = static::compute_style_properties( $feature, $settings, null, $this->theme_json ); - - // Merge new declarations with any that already exist for - // the feature selector. This may occur when multiple block - // support features use the same custom selector. - if ( isset( $feature_declarations[ $feature_selector ] ) ) { - foreach ( $new_feature_declarations as $new_feature_declaration ) { - $feature_declarations[ $feature_selector ][] = $new_feature_declaration; - } - } else { - $feature_declarations[ $feature_selector ] = $new_feature_declarations; - } - - // Remove the feature from the block's node now the - // styles will be included under the feature level selector. - unset( $node[ $feature_name ] ); - } - } - } + $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node ); // If there are style variations, generate the declarations for them, including any feature selectors the block may have. $style_variation_declarations = array(); if ( ! empty( $block_metadata['variations'] ) ) { foreach ( $block_metadata['variations'] as $style_variation ) { - $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); - $style_variation_selector = $style_variation['selector']; - - // If the block has feature selectors, generate the declarations for them within the current style variation. - if ( ! empty( $block_metadata['features'] ) ) { - $clean_style_variation_selector = trim( $style_variation_selector ); - foreach ( $block_metadata['features'] as $feature_name => $feature_selector ) { - if ( empty( $style_variation_node[ $feature_name ] ) ) { - continue; - } - // If feature selector includes block classname, remove it but leave the whitespace in. - $shortened_feature_selector = str_replace( $block_metadata['selector'] . ' ', ' ', $feature_selector ); - // Prepend the variation selector to the feature selector. - $split_feature_selectors = explode( ',', $shortened_feature_selector ); - $feature_selectors = array_map( - static function( $split_feature_selector ) use ( $clean_style_variation_selector ) { - return $clean_style_variation_selector . $split_feature_selector; - }, - $split_feature_selectors - ); - $combined_feature_selectors = implode( ',', $feature_selectors ); - - // Compute declarations for the feature. - $new_feature_declarations = static::compute_style_properties( array( $feature_name => $style_variation_node[ $feature_name ] ), $settings, null, $this->theme_json ); - - /* - * Merge new declarations with any that already exist for - * the feature selector. This may occur when multiple block - * support features use the same custom selector. - */ - if ( isset( $style_variation_declarations[ $combined_feature_selectors ] ) ) { - $style_variation_declarations[ $combined_feature_selectors ] = array_merge( $style_variation_declarations[ $combined_feature_selectors ], $new_feature_declarations ); - } else { - $style_variation_declarations[ $combined_feature_selectors ] = $new_feature_declarations; - } + $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); + $clean_style_variation_selector = trim( $style_variation['selector'] ); + + // Generate any feature/subfeature style declarations for the current style variation. + $variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node ); + + // Combine selectors with style variation's selector and add to overall style variation declarations. + foreach ( $variation_declarations as $current_selector => $new_declarations ) { + // If current selector includes block classname, remove it but leave the whitespace in. + $shortened_selector = str_replace( $block_metadata['selector'] . ' ', ' ', $current_selector ); + + // Prepend the variation selector to the current selector. + $split_selectors = explode( ',', $shortened_selector ); + $updated_selectors = array_map( + static function( $split_selector ) use ( $clean_style_variation_selector ) { + return $clean_style_variation_selector . $split_selector; + }, + $split_selectors + ); + $combined_selectors = implode( ',', $updated_selectors ); - /* - * Remove the feature from the variation's node now the - * styles will be included under the feature level selector. - */ - unset( $style_variation_node[ $feature_name ] ); - } + // Add the new declarations to the overall results under the modified selector. + $style_variation_declarations[ $combined_selectors ] = $new_declarations; } + // Compute declarations for remaining styles not covered by feature level selectors. - $style_variation_declarations[ $style_variation_selector ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json ); + $style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json ); } } @@ -3480,4 +3401,151 @@ public function set_spacing_sizes() { _wp_array_set( $this->theme_json, array( 'settings', 'spacing', 'spacingSizes', 'default' ), $spacing_sizes ); } + + /** + * Returns the selectors metadata for a block. + * + * @param object $block_type The block type. + * @param string $root_selector The block's root selector. + * + * @return object The custom selectors set by the block. + */ + protected static function get_block_selectors( $block_type, $root_selector ) { + if ( ! empty( $block_type->selectors ) ) { + return $block_type->selectors; + } + + $selectors = array( 'root' => $root_selector ); + foreach ( static::BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS as $key => $feature ) { + $feature_selector = wp_get_block_css_selector( $block_type, $key ); + if ( null !== $feature_selector ) { + $selectors[ $feature ] = array( 'root' => $feature_selector ); + } + } + + return $selectors; + } + + /** + * Generates all the element selectors for a block. + * + * @param string $root_selector The block's root CSS selector. + * @return array The block's element selectors. + */ + protected static function get_block_element_selectors( $root_selector ) { + // Assign defaults, then override those that the block sets by itself. + // If the block selector is compounded, will append the element to each + // individual block selector. + $block_selectors = explode( ',', $root_selector ); + $element_selectors = array(); + + foreach ( static::ELEMENTS as $el_name => $el_selector ) { + $element_selector = array(); + foreach ( $block_selectors as $selector ) { + if ( $selector === $el_selector ) { + $element_selector = array( $el_selector ); + break; + } + $element_selector[] = static::append_to_selector( $el_selector, $selector . ' ', 'left' ); + } + $element_selectors[ $el_name ] = implode( ',', $element_selector ); + } + + return $element_selectors; + } + + /** + * Generates style declarations for a node's features e.g. color, border, + * typography etc, that have custom selectors in their related block's + * metadata. + * + * @param object $metadata The related block metadata containing selectors. + * @param object $node A merged theme.json node for block or variation. + * + * @return array The style declarations for the node's features with custom + * selectors. + */ + protected function get_feature_declarations_for_node( $metadata, &$node ) { + $declarations = array(); + + if ( ! isset( $metadata['selectors'] ) ) { + return $declarations; + } + + $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); + + foreach ( $metadata['selectors'] as $feature => $feature_selectors ) { + // Skip if this is the block's root selector or the block doesn't + // have any styles for the feature. + if ( 'root' === $feature || empty( $node[ $feature ] ) ) { + continue; + } + + if ( is_array( $feature_selectors ) ) { + foreach ( $feature_selectors as $subfeature => $subfeature_selector ) { + if ( 'root' === $subfeature || empty( $node[ $feature ][ $subfeature ] ) ) { + continue; + } + + // Create temporary node containing only the subfeature data + // to leverage existing `compute_style_properties` function. + $subfeature_node = array( + $feature => array( + $subfeature => $node[ $feature ][ $subfeature ], + ), + ); + + // Generate style declarations. + $new_declarations = static::compute_style_properties( $subfeature_node, $settings, null, $this->theme_json ); + + // Merge subfeature declarations into feature declarations. + if ( isset( $declarations[ $subfeature_selector ] ) ) { + foreach ( $new_declarations as $new_declaration ) { + $declarations[ $subfeature_selector ][] = $new_declaration; + } + } else { + $declarations[ $subfeature_selector ] = $new_declarations; + } + + // Remove the subfeature from the block's node now its + // styles will be included under its own selector not the + // block's. + unset( $node[ $feature ][ $subfeature ] ); + } + } + + // Now subfeatures have been processed and removed we can process + // feature root selector or simple string selector. + if ( + is_string( $feature_selectors ) || + ( isset( $feature_selectors['root'] ) && $feature_selectors['root'] ) + ) { + $feature_selector = is_string( $feature_selectors ) ? $feature_selectors : $feature_selectors['root']; + + // Create temporary node containing only the feature data + // to leverage existing `compute_style_properties` function. + $feature_node = array( $feature => $node[ $feature ] ); + + // Generate the style declarations. + $new_declarations = static::compute_style_properties( $feature_node, $settings, null, $this->theme_json ); + + // Merge new declarations with any that already exist for + // the feature selector. This may occur when multiple block + // support features use the same custom selector. + if ( isset( $declarations[ $feature_selector ] ) ) { + foreach ( $new_declarations as $new_declaration ) { + $declarations[ $feature_selector ][] = $new_declaration; + } + } else { + $declarations[ $feature_selector ] = $new_declarations; + } + + // Remove the feature from the block's node now its styles + // will be included under its own selector not the block's. + unset( $node[ $feature ] ); + } + } + + return $declarations; + } } diff --git a/lib/compat/wordpress-6.1/blocks.php b/lib/compat/wordpress-6.1/blocks.php index 8ee0acba6b6cfe..908cf3f088eb95 100644 --- a/lib/compat/wordpress-6.1/blocks.php +++ b/lib/compat/wordpress-6.1/blocks.php @@ -16,6 +16,8 @@ function gutenberg_safe_style_attrs_6_1( $attrs ) { $attrs[] = 'flex-wrap'; $attrs[] = 'gap'; + $attrs[] = 'column-gap'; + $attrs[] = 'row-gap'; $attrs[] = 'margin-block-start'; $attrs[] = 'margin-block-end'; $attrs[] = 'margin-inline-start'; diff --git a/lib/compat/wordpress-6.1/template-parts-screen.php b/lib/compat/wordpress-6.1/template-parts-screen.php index 1555b6ab1e250b..7c370635fe2ba0 100644 --- a/lib/compat/wordpress-6.1/template-parts-screen.php +++ b/lib/compat/wordpress-6.1/template-parts-screen.php @@ -133,7 +133,7 @@ static function( $classes ) { } } - $active_global_styles_id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id(); + $active_global_styles_id = WP_Theme_JSON_Resolver_Gutenberg::get_user_global_styles_post_id(); $active_theme = get_stylesheet(); $preload_paths = array( array( '/wp/v2/media', 'OPTIONS' ), diff --git a/lib/compat/wordpress-6.2/block-patterns.php b/lib/compat/wordpress-6.2/block-patterns.php index 9a61041ee291b4..2112a8c2c890f1 100644 --- a/lib/compat/wordpress-6.2/block-patterns.php +++ b/lib/compat/wordpress-6.2/block-patterns.php @@ -433,7 +433,7 @@ function gutenberg_register_remote_theme_patterns() { return; } - $pattern_settings = WP_Theme_JSON_Resolver::get_theme_data()->get_patterns(); + $pattern_settings = gutenberg_get_remote_theme_patterns(); if ( empty( $pattern_settings ) ) { return; } diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php new file mode 100644 index 00000000000000..5f017997f52b58 --- /dev/null +++ b/lib/compat/wordpress-6.3/blocks.php @@ -0,0 +1,28 @@ += 6.3. + * + * @see https://github.com/WordPress/gutenberg/pull/46496 + * + * @param array $settings Current block type settings. + * @param array $metadata Block metadata as read in via block.json. + * + * @return array Filtered block type settings. + */ +function gutenberg_add_selectors_property_to_block_type_settings( $settings, $metadata ) { + if ( ! isset( $settings['selectors'] ) && isset( $metadata['selectors'] ) ) { + $settings['selectors'] = $metadata['selectors']; + } + + return $settings; +} +add_filter( 'block_type_metadata_settings', 'gutenberg_add_selectors_property_to_block_type_settings', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php new file mode 100644 index 00000000000000..c213e50d64ae7b --- /dev/null +++ b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php @@ -0,0 +1,155 @@ +selectors ); + + // Duotone (No fallback selectors for Duotone). + if ( 'filter.duotone' === $target || array( 'filter', 'duotone' ) === $target ) { + // If selectors API in use, only use it's value or null. + if ( $has_selectors ) { + return _wp_array_get( $block_type->selectors, array( 'filter', 'duotone' ), null ); + } + + // Selectors API, not available, check for old experimental selector. + return _wp_array_get( $block_type->supports, array( 'color', '__experimentalDuotone' ), null ); + } + + // Root Selector. + + // Calculated before returning as it can be used as fallback for + // feature selectors later on. + $root_selector = null; + + if ( $has_selectors && isset( $block_type->selectors['root'] ) ) { + // Use the selectors API if available. + $root_selector = $block_type->selectors['root']; + } elseif ( isset( $block_type->supports['__experimentalSelector'] ) && is_string( $block_type->supports['__experimentalSelector'] ) ) { + // Use the old experimental selector supports property if set. + $root_selector = $block_type->supports['__experimentalSelector']; + } else { + // If no root selector found, generate default block class selector. + $block_name = str_replace( '/', '-', str_replace( 'core/', '', $block_type->name ) ); + $root_selector = ".wp-block-{$block_name}"; + } + + // Return selector if it's the root target we are looking for. + if ( 'root' === $target ) { + return $root_selector; + } + + // If target is not `root` or `duotone` we have a feature or subfeature + // as the target. If the target is a string convert to an array. + if ( is_string( $target ) ) { + $target = explode( '.', $target ); + } + + // Feature Selectors ( May fallback to root selector ). + if ( 1 === count( $target ) ) { + $fallback_selector = $fallback ? $root_selector : null; + + // Prefer the selectors API if available. + if ( $has_selectors ) { + // Look for selector under `feature.root`. + $path = array_merge( $target, array( 'root' ) ); + $feature_selector = _wp_array_get( $block_type->selectors, $path, null ); + + if ( $feature_selector ) { + return $feature_selector; + } + + // Check if feature selector is set via shorthand. + $feature_selector = _wp_array_get( $block_type->selectors, $target, null ); + + return is_string( $feature_selector ) ? $feature_selector : $fallback_selector; + } + + // Try getting old experimental supports selector value. + $path = array_merge( $target, array( '__experimentalSelector' ) ); + $feature_selector = _wp_array_get( $block_type->supports, $path, null ); + + // Nothing to work with, provide fallback or null. + if ( null === $feature_selector ) { + return $fallback_selector; + } + + // Scope the feature selector by the block's root selector. + $scopes = explode( ',', $root_selector ); + $selectors = explode( ',', $feature_selector ); + + $selectors_scoped = array(); + foreach ( $scopes as $outer ) { + foreach ( $selectors as $inner ) { + $outer = trim( $outer ); + $inner = trim( $inner ); + if ( ! empty( $outer ) && ! empty( $inner ) ) { + $selectors_scoped[] = $outer . ' ' . $inner; + } elseif ( empty( $outer ) ) { + $selectors_scoped[] = $inner; + } elseif ( empty( $inner ) ) { + $selectors_scoped[] = $outer; + } + } + } + + return implode( ', ', $selectors_scoped ); + } + + // Subfeature selector + // This may fallback either to parent feature or root selector. + $subfeature_selector = null; + + // Use selectors API if available. + if ( $has_selectors ) { + $subfeature_selector = _wp_array_get( $block_type->selectors, $target, null ); + } + + // Only return if we have a subfeature selector. + if ( $subfeature_selector ) { + return $subfeature_selector; + } + + // To this point we don't have a subfeature selector. If a fallback + // has been requested, remove subfeature from target path and return + // results of a call for the parent feature's selector. + if ( $fallback ) { + return wp_get_block_css_selector( $block_type, $target[0], $fallback ); + } + + // We tried... + return null; + } +} + +/** + * Returns the current theme's wanted patterns(slugs) to be + * registered from Pattern Directory. + * + * @since 6.3.0 + * + * @return string[] + */ +function gutenberg_get_remote_theme_patterns() { + return WP_Theme_JSON_Resolver_Gutenberg::get_theme_data( array(), array( 'with_supports' => false ) )->get_patterns(); +} diff --git a/lib/compat/wordpress-6.3/rest-api.php b/lib/compat/wordpress-6.3/rest-api.php index 93422289100dfa..f8f5fc91fed3d3 100644 --- a/lib/compat/wordpress-6.3/rest-api.php +++ b/lib/compat/wordpress-6.3/rest-api.php @@ -22,7 +22,7 @@ function gutenberg_register_rest_pattern_directory() { * @param string $post_type Post type key. */ function gutenberg_update_templates_template_parts_rest_controller( $args, $post_type ) { - if ( in_array( $post_type, array( 'wp_template', 'wp_template-part' ), true ) ) { + if ( in_array( $post_type, array( 'wp_template', 'wp_template_part' ), true ) ) { $args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller_6_3'; } return $args; diff --git a/lib/compat/wordpress-6.3/script-loader.php b/lib/compat/wordpress-6.3/script-loader.php new file mode 100644 index 00000000000000..8a8e41ef2be919 --- /dev/null +++ b/lib/compat/wordpress-6.3/script-loader.php @@ -0,0 +1,15 @@ +compile_src( $font['font-family'], $value ); + $value = $this->compile_src( $value ); } // If font-variation-settings is an array, convert it to a string. @@ -200,11 +200,10 @@ private function build_font_face_css( array $font ) { * * @since X.X.X * - * @param string $font_family Font family. - * @param array $value Value to process. + * @param array $value Value to process. * @return string The CSS. */ - private function compile_src( $font_family, array $value ) { + private function compile_src( array $value ) { $src = ''; foreach ( $value as $item ) { diff --git a/lib/experimental/kses.php b/lib/experimental/kses.php index a79ef5dbdce42a..1138aa67933ef0 100644 --- a/lib/experimental/kses.php +++ b/lib/experimental/kses.php @@ -87,3 +87,22 @@ function allow_filter_in_styles( $allow_css, $css_test_string ) { } add_filter( 'safecss_filter_attr_allow_css', 'allow_filter_in_styles', 10, 2 ); + +/** + * Mark CSS safe if it contains grid functions + * + * This function should not be backported to core. + * + * @param bool $allow_css Whether the CSS is allowed. + * @param string $css_test_string The CSS to test. + */ +function allow_grid_functions_in_styles( $allow_css, $css_test_string ) { + if ( preg_match( + '/^grid-template-columns:\s*repeat\([0-9,a-z-\s\(\)]*\)$/', + $css_test_string + ) ) { + return true; + } + return $allow_css; +} +add_filter( 'safecss_filter_attr_allow_css', 'allow_grid_functions_in_styles', 10, 2 ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 0304b851c19e5a..0e9fc89aca1559 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -65,6 +65,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-group-grid-variation', + __( 'Grid variation for Group block ', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test the Grid layout type as a new variation of Group block.', 'gutenberg' ), + 'id' => 'gutenberg-group-grid-variation', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/lib/load.php b/lib/load.php index a5be015320652f..e281aebd0d1411 100644 --- a/lib/load.php +++ b/lib/load.php @@ -93,6 +93,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.2/widgets.php'; require __DIR__ . '/compat/wordpress-6.2/menu.php'; +// WordPress 6.3 compat. +require __DIR__ . '/compat/wordpress-6.3/get-global-styles-and-settings.php'; + if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-attribute-token.php'; require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-span.php'; @@ -102,6 +105,8 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.3 compat. require __DIR__ . '/compat/wordpress-6.3/html-api/class-gutenberg-html-tag-processor-6-3.php'; +require __DIR__ . '/compat/wordpress-6.3/script-loader.php'; +require __DIR__ . '/compat/wordpress-6.3/blocks.php'; // Experimental features. remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WP 6.0's stopgap handler for Webfonts API. @@ -131,6 +136,7 @@ function gutenberg_is_experiment_enabled( $name ) { // Plugin specific code. require __DIR__ . '/class-wp-theme-json-gutenberg.php'; require __DIR__ . '/class-wp-theme-json-resolver-gutenberg.php'; +require __DIR__ . '/class-wp-duotone-gutenberg.php'; require __DIR__ . '/blocks.php'; require __DIR__ . '/client-assets.php'; require __DIR__ . '/demo.php'; diff --git a/lib/theme.json b/lib/theme.json index fa3a872518adc7..8928c1f0d8d89f 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -11,6 +11,8 @@ }, "color": { "background": true, + "button": true, + "caption": true, "custom": true, "customDuotone": true, "customGradient": true, @@ -121,6 +123,7 @@ "slug": "midnight" } ], + "heading": true, "link": false, "palette": [ { @@ -352,6 +355,28 @@ } } ] + }, + "grid": { + "name": "grid", + "slug": "grid", + "className": "is-layout-grid", + "displayMode": "grid", + "baseStyles": [ + { + "selector": " > *", + "rules": { + "margin": "0" + } + } + ], + "spacingStyles": [ + { + "selector": "", + "rules": { + "gap": null + } + } + ] } } }, diff --git a/package-lock.json b/package-lock.json index 0ad5466ceb1068..6b7971a895f459 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.4.0-rc.1", + "version": "15.5.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6857,19 +6857,20 @@ } }, "@playwright/test": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz", - "integrity": "sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz", + "integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==", "dev": true, "requires": { "@types/node": "*", - "playwright-core": "1.30.0" + "fsevents": "2.3.2", + "playwright-core": "1.32.0" }, "dependencies": { "playwright-core": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz", - "integrity": "sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.0.tgz", + "integrity": "sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==", "dev": true } } @@ -15260,6 +15261,11 @@ "@types/node": "*" } }, + "@types/gradient-parser": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/gradient-parser/-/gradient-parser-0.1.2.tgz", + "integrity": "sha512-e2s1svCYJY8JDr2t/OoB/H4aWZy4sXUOAZ0NdSSHjKACw1jeU54gf4xj38di0AgVIObgzN1JSikJ5oSo3vxwgA==" + }, "@types/hammerjs": { "version": "2.0.41", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz", @@ -17534,6 +17540,7 @@ "@wordpress/element": "file:packages/element", "@wordpress/hooks": "file:packages/hooks", "@wordpress/icons": "file:packages/icons", + "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "memize": "^1.1.0" } }, @@ -17903,7 +17910,7 @@ "npm-packlist": "^3.0.0", "postcss": "^8.4.5", "postcss-loader": "^6.2.1", - "prettier": "npm:wp-prettier@2.6.2", + "prettier": "npm:wp-prettier@2.8.5", "puppeteer-core": "^13.2.0", "react-refresh": "^0.10.0", "read-pkg-up": "^7.0.1", @@ -42983,6 +42990,15 @@ } } }, + "mock-match-media": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/mock-match-media/-/mock-match-media-0.4.2.tgz", + "integrity": "sha512-1Q/Z7cfqWVTZd5Iz0bLxqzl6/8vaPl4KxxciRSRu+TCeGxRlxGDgjwWre0KkHlfS01r1iGCUWuF0n3ftvS/C/A==", + "dev": true, + "requires": { + "css-mediaquery": "^0.1.2" + } + }, "modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", @@ -47023,9 +47039,9 @@ "dev": true }, "prettier": { - "version": "npm:wp-prettier@2.6.2", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.6.2.tgz", - "integrity": "sha512-AV33EzqiFJ3fj+mPlKABN59YFPReLkDxQnj067Z3uEOeRQf3g05WprL0RDuqM7UBhSRo9W1rMSC2KvZmjE5UOA==", + "version": "npm:wp-prettier@2.8.5", + "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.8.5.tgz", + "integrity": "sha512-gkphzYtVksWV6D7/V530bTehKkhrABUru/Gy4reOLOHJoKH4i9lcE1SxqU2VDxC3gCOx/Nk9alZmWk6xL/IBCw==", "dev": true }, "prettier-linter-helpers": { diff --git a/package.json b/package.json index 16b1e5478354cd..4b2cc41d2f566f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "15.4.0-rc.1", + "version": "15.5.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -22,6 +22,7 @@ "IS_GUTENBERG_PLUGIN": true }, "dependencies": { + "@types/gradient-parser": "0.1.2", "@wordpress/a11y": "file:packages/a11y", "@wordpress/annotations": "file:packages/annotations", "@wordpress/api-fetch": "file:packages/api-fetch", @@ -98,7 +99,7 @@ "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.6.0", - "@playwright/test": "1.30.0", + "@playwright/test": "1.32.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.2", "@storybook/addon-a11y": "6.5.7", "@storybook/addon-actions": "6.5.7", @@ -206,6 +207,7 @@ "metro-react-native-babel-preset": "0.70.3", "metro-react-native-babel-transformer": "0.70.3", "mkdirp": "0.5.1", + "mock-match-media": "0.4.2", "nock": "12.0.3", "node-fetch": "2.6.1", "node-watch": "0.7.0", @@ -213,7 +215,7 @@ "patch-package": "6.2.2", "postcss": "8.4.16", "postcss-loader": "6.2.1", - "prettier": "npm:wp-prettier@2.6.2", + "prettier": "npm:wp-prettier@2.8.5", "progress": "2.0.3", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/packages/api-fetch/README.md b/packages/api-fetch/README.md index ae7b1a500aa4f8..9fd172cdd9f9b9 100644 --- a/packages/api-fetch/README.md +++ b/packages/api-fetch/README.md @@ -14,15 +14,29 @@ _This package assumes that your code will run in an **ES2015+** environment. If ## Usage +### GET ```js import apiFetch from '@wordpress/api-fetch'; -// GET apiFetch( { path: '/wp/v2/posts' } ).then( ( posts ) => { console.log( posts ); } ); +``` + +### GET with Query Args +```js +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +const queryParams = { include: [1,2,3] }; // Return posts with ID = 1,2,3. -// POST +apiFetch( { path: addQueryArgs( '/wp/v2/posts', queryParams } ).then( ( posts ) => { + console.log( posts ); +} ); +``` + +### POST +```js apiFetch( { path: '/wp/v2/posts/1', method: 'POST', @@ -52,7 +66,7 @@ Unlike `fetch`, the `Promise` return value of `apiFetch` will resolve to the par #### `data` (`object`) -Shorthand to be used in place of `body`, accepts an object value to be stringified to JSON. +Sent on `POST` or `PUT` requests only. Shorthand to be used in place of `body`, accepts an object value to be stringified to JSON. ### Aborting a request diff --git a/packages/babel-plugin-makepot/index.js b/packages/babel-plugin-makepot/index.js index 846a29a7955b34..0ae8a763d03359 100644 --- a/packages/babel-plugin-makepot/index.js +++ b/packages/babel-plugin-makepot/index.js @@ -342,7 +342,7 @@ module.exports = () => { if ( isSameTranslation( translation, - memo[ msgctxt ][ msgid ] + memo[ msgctxt ][ msgid ] ?? {} ) ) { translation.comments.reference = [ diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index 61974d2d8b47f6..39c375ef48a1e5 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancement + +- Exclude IE-only `setImmediate`/`clearImmediate` from list of polyfills. + ## 7.13.0 (2023-03-15) ## 7.12.0 (2023-03-01) diff --git a/packages/babel-preset-default/bin/index.js b/packages/babel-preset-default/bin/index.js index 594ae4744a8a34..a2d86233565138 100755 --- a/packages/babel-preset-default/bin/index.js +++ b/packages/babel-preset-default/bin/index.js @@ -9,11 +9,16 @@ const { writeFile } = require( 'fs' ).promises; builder( { modules: [ 'es', 'web' ], - // core-js is extremely conservative in which polyfills to include. - // Knowing about tiny browser implementation bugs that anyone rarely cares about, - // we prevent some features from having the full polyfill included. - // @see https://github.com/WordPress/gutenberg/pull/31279 - exclude: [ 'es.promise' ], + exclude: [ + // core-js is extremely conservative in which polyfills to include. + // Since we don't care about the tiny browser implementation bugs behind its decision + // to polyfill these features, we forcefully prevent them from being included. + // @see https://github.com/WordPress/gutenberg/pull/31279 + 'es.promise', + // This is an IE-only feature which we don't use, and don't want to polyfill. + // @see https://github.com/WordPress/gutenberg/pull/49234 + 'web.immediate', + ], targets: require( '@wordpress/browserslist-config' ), filename: './build/polyfill.js', } ) diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 97427da1fd525e..d61403d7027073 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -170,6 +170,11 @@ $z-layers: ( // Needs to be higher than .components-circular-option-picker__option.is-pressed. ".components-circular-option-picker__option.is-pressed + svg": 2, + // The following two indexes are needed so that the swatches (and their tooltips) + // always render on top of the rest of the component's UI. + ".components-circular-option-picker__swatches": 1, + "> *:not(.components-circular-option-picker__swatches)": 0, + // Appear under the customizer heading UI, but over anything else. ".customize-widgets__topbar": 8, diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index e33f743838286f..7aa68696cc0b99 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- `ImageSizeControl`: Update image size label ([#49112](https://github.com/WordPress/gutenberg/pull/49112)). + ## 11.6.0 (2023-03-15) ## 11.5.0 (2023-03-01) diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 18c2c2cfbce521..980d70b4108736 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -76,6 +76,10 @@ registerCoreBlocks(); ## API +Any components in this package that have a counterpart in [@wordpress/components](/packages/components/README.md) are an extension of those components. + +Unless you're [creating an editor](docs/how-to-guides/platform/custom-block-editor/README.md), it is recommended that the components in @wordpress/components should be used rather than the ones in this package as these components have been customized for use in an editor and may result in unexpected behaviour if used outside of this context. + ### AlignmentControl diff --git a/packages/block-editor/src/components/block-draggable/content.scss b/packages/block-editor/src/components/block-draggable/content.scss index 46bd879806d785..f1318daebd5a04 100644 --- a/packages/block-editor/src/components/block-draggable/content.scss +++ b/packages/block-editor/src/components/block-draggable/content.scss @@ -1,5 +1,5 @@ // This creates a "slot" where the block you're dragging appeared. -// We use !important as one of the rules are meant to be overriden. +// We use !important as one of the rules are meant to be overridden. .block-editor-block-list__layout .is-dragging { background-color: currentColor !important; opacity: 0.05 !important; diff --git a/packages/block-editor/src/components/block-list/block-html.js b/packages/block-editor/src/components/block-list/block-html.js index ef38df983740a0..5d9e7bfeb7bf77 100644 --- a/packages/block-editor/src/components/block-list/block-html.js +++ b/packages/block-editor/src/components/block-list/block-html.js @@ -59,7 +59,7 @@ function BlockHTML( { clientId } ) { // Ensure the state is updated if we reset so it displays the default content. if ( ! html ) { - setHtml( { content } ); + setHtml( content ); } }; diff --git a/packages/block-editor/src/components/block-preview/auto.js b/packages/block-editor/src/components/block-preview/auto.js index 287bcd5b185662..ef7aa76e5af02e 100644 --- a/packages/block-editor/src/components/block-preview/auto.js +++ b/packages/block-editor/src/components/block-preview/auto.js @@ -12,7 +12,6 @@ import { Disabled } from '@wordpress/components'; import BlockList from '../block-list'; import Iframe from '../iframe'; import EditorStyles from '../editor-styles'; -import { __unstablePresetDuotoneFilter as PresetDuotoneFilter } from '../../components/duotone'; import { store } from '../../store'; // This is used to avoid rendering the block list if the sizes change. @@ -32,11 +31,10 @@ function ScaledBlockPreview( { const [ contentResizeListener, { height: contentHeight } ] = useResizeObserver(); - const { styles, duotone } = useSelect( ( select ) => { + const { styles } = useSelect( ( select ) => { const settings = select( store ).getSettings(); return { styles: settings.styles, - duotone: settings.__experimentalFeatures?.color?.duotone, }; }, [] ); @@ -56,10 +54,6 @@ function ScaledBlockPreview( { return styles; }, [ styles, additionalStyles ] ); - const svgFilters = useMemo( () => { - return [ ...( duotone?.default ?? [] ), ...( duotone?.theme ?? [] ) ]; - }, [ duotone ] ); - // Initialize on render instead of module top level, to avoid circular dependency issues. MemoizedBlockList = MemoizedBlockList || pure( BlockList ); @@ -76,7 +70,6 @@ function ScaledBlockPreview( { } } > diff --git a/packages/block-editor/src/components/colors-gradients/style.scss b/packages/block-editor/src/components/colors-gradients/style.scss index 5d86252105acfe..8b36f04b835526 100644 --- a/packages/block-editor/src/components/colors-gradients/style.scss +++ b/packages/block-editor/src/components/colors-gradients/style.scss @@ -73,20 +73,20 @@ $swatch-gap: 12px; max-width: 100%; // Border styles. - border-left: 1px solid rgba(0, 0, 0, 0.1); - border-right: 1px solid rgba(0, 0, 0, 0.1); - border-bottom: 1px solid rgba(0, 0, 0, 0.1); + border-left: 1px solid $gray-300; + border-right: 1px solid $gray-300; + border-bottom: 1px solid $gray-300; &.first { margin-top: $grid-unit-30; - border-top-left-radius: 2px; - border-top-right-radius: 2px; - border-top: 1px solid rgba(0, 0, 0, 0.1); + border-top-left-radius: $radius-block-ui; + border-top-right-radius: $radius-block-ui; + border-top: 1px solid $gray-300; } &.last { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; + border-bottom-left-radius: $radius-block-ui; + border-bottom-right-radius: $radius-block-ui; } > div, diff --git a/packages/block-editor/src/components/editor-styles/index.js b/packages/block-editor/src/components/editor-styles/index.js index b6288f1afb452d..819efe5721127a 100644 --- a/packages/block-editor/src/components/editor-styles/index.js +++ b/packages/block-editor/src/components/editor-styles/index.js @@ -8,6 +8,7 @@ import a11yPlugin from 'colord/plugins/a11y'; /** * WordPress dependencies */ +import { SVG } from '@wordpress/components'; import { useCallback, useMemo } from '@wordpress/element'; /** @@ -68,7 +69,20 @@ function useDarkThemeBodyClassName( styles ) { export default function EditorStyles( { styles } ) { const transformedStyles = useMemo( - () => transformStyles( styles, EDITOR_STYLES_SELECTOR ), + () => + transformStyles( + styles.filter( ( style ) => style?.css ), + EDITOR_STYLES_SELECTOR + ), + [ styles ] + ); + + const transformedSvgs = useMemo( + () => + styles + .filter( ( style ) => style.__unstableType === 'svgs' ) + .map( ( style ) => style.assets ) + .join( '' ), [ styles ] ); @@ -80,6 +94,20 @@ export default function EditorStyles( { styles } ) { { transformedStyles.map( ( css, index ) => ( ) ) } + ); } diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js new file mode 100644 index 00000000000000..ba8a97b1708584 --- /dev/null +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -0,0 +1,706 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + __experimentalHStack as HStack, + __experimentalZStack as ZStack, + __experimentalDropdownContentWrapper as DropdownContentWrapper, + TabPanel, + ColorIndicator, + Flex, + FlexItem, + Dropdown, + Button, +} from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import ColorGradientControl from '../colors-gradients/control'; +import { useColorsPerOrigin, useGradientsPerOrigin } from './hooks'; +import { getValueFromVariable } from './utils'; +import { immutableSet } from '../../utils/object'; + +export function useHasColorPanel( settings ) { + const hasTextPanel = useHasTextPanel( settings ); + const hasBackgroundPanel = useHasBackgroundPanel( settings ); + const hasLinkPanel = useHasLinkPanel( settings ); + const hasHeadingPanel = useHasHeadingPanel( settings ); + const hasButtonPanel = useHasHeadingPanel( settings ); + const hasCaptionPanel = useHasCaptionPanel( settings ); + + return ( + hasTextPanel || + hasBackgroundPanel || + hasLinkPanel || + hasHeadingPanel || + hasButtonPanel || + hasCaptionPanel + ); +} + +export function useHasTextPanel( settings ) { + const colors = useColorsPerOrigin( settings ); + return ( + settings?.color?.text && + ( colors?.length > 0 || settings?.color?.custom ) + ); +} + +export function useHasLinkPanel( settings ) { + const colors = useColorsPerOrigin( settings ); + return ( + settings?.color?.link && + ( colors?.length > 0 || settings?.color?.custom ) + ); +} + +export function useHasCaptionPanel( settings ) { + const colors = useColorsPerOrigin( settings ); + return ( + settings?.color?.caption && + ( colors?.length > 0 || settings?.color?.custom ) + ); +} + +export function useHasHeadingPanel( settings ) { + const colors = useColorsPerOrigin( settings ); + const gradients = useGradientsPerOrigin( settings ); + return ( + settings?.color?.heading && + ( colors?.length > 0 || + settings?.color?.custom || + gradients?.length > 0 || + settings?.color?.customGradient ) + ); +} + +export function useHasButtonPanel( settings ) { + const colors = useColorsPerOrigin( settings ); + const gradients = useGradientsPerOrigin( settings ); + return ( + settings?.color?.button && + ( colors?.length > 0 || + settings?.color?.custom || + gradients?.length > 0 || + settings?.color?.customGradient ) + ); +} + +export function useHasBackgroundPanel( settings ) { + const colors = useColorsPerOrigin( settings ); + const gradients = useGradientsPerOrigin( settings ); + return ( + settings?.color?.background && + ( colors?.length > 0 || + settings?.color?.custom || + gradients?.length > 0 || + settings?.color?.customGradient ) + ); +} + +function ColorToolsPanel( { + resetAllFilter, + onChange, + value, + panelId, + children, +} ) { + const resetAll = () => { + const updatedValue = resetAllFilter( value ); + onChange( updatedValue ); + }; + + return ( + +
+ { children } +
+
+ ); +} + +const DEFAULT_CONTROLS = { + text: true, + background: true, + link: true, + heading: true, + button: true, + caption: true, +}; + +const popoverProps = { + placement: 'left-start', + offset: 36, + shift: true, +}; + +const LabeledColorIndicators = ( { indicators, label } ) => ( + + + { indicators.map( ( indicator, index ) => ( + + + + ) ) } + + + { label } + + +); + +function ColorPanelTab( { + isGradient, + inheritedValue, + userValue, + setValue, + colorGradientControlSettings, +} ) { + return ( + + ); +} + +function ColorPanelDropdown( { + label, + hasValue, + resetValue, + isShownByDefault, + indicators, + tabs, + colorGradientControlSettings, + panelId, +} ) { + const tabConfigs = tabs.map( ( { key, label: tabLabel } ) => { + return { + name: key, + title: tabLabel, + }; + } ); + + return ( + + { + const toggleProps = { + onClick: onToggle, + className: classnames( + 'block-editor-panel-color-gradient-settings__dropdown', + { 'is-open': isOpen } + ), + 'aria-expanded': isOpen, + }; + + return ( + + ); + } } + renderContent={ () => ( + +
+ { tabs.length === 1 && ( + + ) } + { tabs.length > 1 && ( + + { ( tab ) => { + const selectedTab = tabs.find( + ( t ) => t.key === tab.name + ); + + if ( ! selectedTab ) { + return null; + } + + return ( + + ); + } } + + ) } +
+
+ ) } + /> +
+ ); +} + +export default function ColorPanel( { + as: Wrapper = ColorToolsPanel, + value, + onChange, + inheritedValue = value, + settings, + panelId, + defaultControls = DEFAULT_CONTROLS, + children, +} ) { + const colors = useColorsPerOrigin( settings ); + const gradients = useGradientsPerOrigin( settings ); + const areCustomSolidsEnabled = settings?.color?.custom; + const areCustomGradientsEnabled = settings?.color?.customGradient; + const hasSolidColors = colors.length > 0 || areCustomSolidsEnabled; + const hasGradientColors = gradients.length > 0 || areCustomGradientsEnabled; + const decodeValue = ( rawValue ) => + getValueFromVariable( { settings }, '', rawValue ); + const encodeColorValue = ( colorValue ) => { + const allColors = colors.flatMap( + ( { colors: originColors } ) => originColors + ); + const colorObject = allColors.find( + ( { color } ) => color === colorValue + ); + return colorObject + ? 'var:preset|color|' + colorObject.slug + : colorValue; + }; + const encodeGradientValue = ( gradientValue ) => { + const allGradients = gradients.flatMap( + ( { gradients: originGradients } ) => originGradients + ); + const gradientObject = allGradients.find( + ( { gradient } ) => gradient === gradientValue + ); + return gradientObject + ? 'var:preset|gradient|' + gradientObject.slug + : gradientValue; + }; + + // Text Color + const showTextPanel = useHasTextPanel( settings ); + const textColor = decodeValue( inheritedValue?.color?.text ); + const userTextColor = decodeValue( value?.color?.text ); + const hasTextColor = () => !! userTextColor; + const setTextColor = ( newColor ) => { + onChange( + immutableSet( + value, + [ 'color', 'text' ], + encodeColorValue( newColor ) + ) + ); + }; + const resetTextColor = () => setTextColor( undefined ); + + // BackgroundColor + const showBackgroundPanel = useHasBackgroundPanel( settings ); + const backgroundColor = decodeValue( inheritedValue?.color?.background ); + const userBackgroundColor = decodeValue( value?.color?.background ); + const gradient = decodeValue( inheritedValue?.color?.gradient ); + const userGradient = decodeValue( value?.color?.gradient ); + const hasBackground = () => !! userBackgroundColor || !! userGradient; + const setBackgroundColor = ( newColor ) => { + const newValue = immutableSet( + value, + [ 'color', 'background' ], + encodeColorValue( newColor ) + ); + newValue.color.gradient = undefined; + onChange( newValue ); + }; + const setGradient = ( newGradient ) => { + const newValue = immutableSet( + value, + [ 'color', 'gradient' ], + encodeGradientValue( newGradient ) + ); + newValue.color.background = undefined; + onChange( newValue ); + }; + const resetBackground = () => { + const newValue = immutableSet( + value, + [ 'color', 'background' ], + undefined + ); + newValue.color.gradient = undefined; + onChange( newValue ); + }; + + // Links + const showLinkPanel = useHasLinkPanel( settings ); + const linkColor = decodeValue( + inheritedValue?.elements?.link?.color?.text + ); + const userLinkColor = decodeValue( value?.elements?.link?.color?.text ); + const setLinkColor = ( newColor ) => { + onChange( + immutableSet( + value, + [ 'elements', 'link', 'color', 'text' ], + encodeColorValue( newColor ) + ) + ); + }; + const hoverLinkColor = decodeValue( + inheritedValue?.elements?.link?.[ ':hover' ]?.color?.text + ); + const userHoverLinkColor = decodeValue( + value?.elements?.link?.[ ':hover' ]?.color?.text + ); + const setHoverLinkColor = ( newColor ) => { + onChange( + immutableSet( + value, + [ 'elements', 'link', ':hover', 'color', 'text' ], + encodeColorValue( newColor ) + ) + ); + }; + const hasLink = () => !! userLinkColor || !! userHoverLinkColor; + const resetLink = () => { + let newValue = immutableSet( + value, + [ 'elements', 'link', ':hover', 'color', 'text' ], + undefined + ); + newValue = immutableSet( + newValue, + [ 'elements', 'link', 'color', 'text' ], + undefined + ); + onChange( newValue ); + }; + + // Elements + const elements = [ + { + name: 'caption', + label: __( 'Captions' ), + showPanel: useHasCaptionPanel( settings ), + }, + { + name: 'button', + label: __( 'Button' ), + showPanel: useHasButtonPanel( settings ), + }, + { + name: 'heading', + label: __( 'Heading' ), + showPanel: useHasHeadingPanel( settings ), + }, + { + name: 'h1', + label: __( 'H1' ), + showPanel: useHasHeadingPanel( settings ), + }, + { + name: 'h2', + label: __( 'H2' ), + showPanel: useHasHeadingPanel( settings ), + }, + { + name: 'h3', + label: __( 'H3' ), + showPanel: useHasHeadingPanel( settings ), + }, + { + name: 'h4', + label: __( 'H4' ), + showPanel: useHasHeadingPanel( settings ), + }, + { + name: 'h5', + label: __( 'H5' ), + showPanel: useHasHeadingPanel( settings ), + }, + { + name: 'h6', + label: __( 'H6' ), + showPanel: useHasHeadingPanel( settings ), + }, + ]; + + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, + color: undefined, + elements: { + ...previousValue?.elements, + link: { + ...previousValue?.elements?.link, + color: undefined, + ':hover': { + color: undefined, + }, + }, + ...elements.reduce( ( acc, element ) => { + return { + ...acc, + [ element.name ]: { + ...previousValue?.elements?.[ element.name ], + color: undefined, + }, + }; + }, {} ), + }, + }; + }, [] ); + + const items = [ + showTextPanel && { + key: 'text', + label: __( 'Text' ), + hasValue: hasTextColor, + resetValue: resetTextColor, + isShownByDefault: defaultControls.text, + indicators: [ textColor ], + tabs: [ + { + key: 'text', + label: __( 'Text' ), + inheritedValue: textColor, + setValue: setTextColor, + userValue: userTextColor, + }, + ], + }, + showBackgroundPanel && { + key: 'background', + label: __( 'Background' ), + hasValue: hasBackground, + resetValue: resetBackground, + isShownByDefault: defaultControls.background, + indicators: [ gradient ?? backgroundColor ], + tabs: [ + { + key: 'background', + label: __( 'Solid' ), + inheritedValue: backgroundColor, + setValue: setBackgroundColor, + userValue: userBackgroundColor, + }, + { + key: 'gradient', + label: __( 'Gradient' ), + inheritedValue: gradient, + setValue: setGradient, + userValue: userGradient, + isGradient: true, + }, + ], + }, + showLinkPanel && { + key: 'link', + label: __( 'Link' ), + hasValue: hasLink, + resetValue: resetLink, + isShownByDefault: defaultControls.link, + indicators: [ linkColor, hoverLinkColor ], + tabs: [ + { + key: 'link', + label: __( 'Default' ), + inheritedValue: linkColor, + setValue: setLinkColor, + userValue: userLinkColor, + }, + { + key: 'hover', + label: __( 'Hover' ), + inheritedValue: hoverLinkColor, + setValue: setHoverLinkColor, + userValue: userHoverLinkColor, + }, + ], + }, + ].filter( Boolean ); + + elements.forEach( ( { name, label, showPanel } ) => { + if ( ! showPanel ) return; + + const elementBackgroundColor = decodeValue( + inheritedValue?.elements?.[ name ]?.color?.background + ); + const elementGradient = decodeValue( + inheritedValue?.elements?.[ name ]?.color?.gradient + ); + const elementTextColor = decodeValue( + inheritedValue?.elements?.[ name ]?.color?.text + ); + const elementBackgroundUserColor = decodeValue( + value?.elements?.[ name ]?.color?.background + ); + const elementGradientUserColor = decodeValue( + value?.elements?.[ name ]?.color?.gradient + ); + const elementTextUserColor = decodeValue( + value?.elements?.[ name ]?.color?.text + ); + const hasElement = () => + !! ( + elementTextUserColor || + elementBackgroundUserColor || + elementGradientUserColor + ); + const resetElement = () => { + const newValue = immutableSet( + value, + [ 'elements', name, 'color', 'background' ], + undefined + ); + newValue.elements[ name ].color.gradient = undefined; + newValue.elements[ name ].color.text = undefined; + onChange( newValue ); + }; + + const setElementTextColor = ( newTextColor ) => { + onChange( + immutableSet( + value, + [ 'elements', name, 'color', 'text' ], + encodeColorValue( newTextColor ) + ) + ); + }; + const setElementBackgroundColor = ( newBackgroundColor ) => { + const newValue = immutableSet( + value, + [ 'elements', name, 'color', 'background' ], + encodeColorValue( newBackgroundColor ) + ); + newValue.elements[ name ].color.gradient = undefined; + onChange( newValue ); + }; + const setElementGradient = ( newGradient ) => { + const newValue = immutableSet( + value, + [ 'elements', name, 'color', 'gradient' ], + encodeGradientValue( newGradient ) + ); + newValue.elements[ name ].color.background = undefined; + onChange( newValue ); + }; + const supportsTextColor = true; + // Background color is not supported for `caption` + // as there isn't yet a way to set padding for the element. + const supportsBackground = name !== 'caption'; + + items.push( { + key: name, + label, + hasValue: hasElement, + resetValue: resetElement, + isShownByDefault: defaultControls[ name ], + indicators: + supportsTextColor && supportsBackground + ? [ + elementTextColor, + elementGradient ?? elementBackgroundColor, + ] + : [ + supportsTextColor + ? elementTextColor + : elementGradient ?? elementBackgroundColor, + ], + tabs: [ + hasSolidColors && + supportsTextColor && { + key: 'text', + label: __( 'Text' ), + inheritedValue: elementTextColor, + setValue: setElementTextColor, + userValue: elementTextUserColor, + }, + hasSolidColors && + supportsBackground && { + key: 'background', + label: __( 'Background' ), + inheritedValue: elementBackgroundColor, + setValue: setElementBackgroundColor, + userValue: elementBackgroundUserColor, + }, + hasGradientColors && + supportsBackground && { + key: 'gradient', + label: __( 'Gradient' ), + inheritedValue: elementGradient, + setValue: setElementGradient, + userValue: elementGradientUserColor, + isGradient: true, + }, + ].filter( Boolean ), + } ); + } ); + + return ( + + { items.map( ( item ) => ( + + ) ) } + { children } + + ); +} diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index b63d9c5d063739..c876ca2d1c1b66 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -28,6 +28,7 @@ import SpacingSizesControl from '../spacing-sizes-control'; import HeightControl from '../height-control'; import ChildLayoutControl from '../child-layout-control'; import { cleanEmptyObject } from '../../hooks/utils'; +import { immutableSet } from '../../utils/object'; const AXIAL_SIDES = [ 'horizontal', 'vertical' ]; @@ -223,13 +224,9 @@ export default function DimensionsPanel( { useHasContentSize( settings ) && includeLayoutControls; const contentSizeValue = decodeValue( inheritedValue?.layout?.contentSize ); const setContentSizeValue = ( newValue ) => { - onChange( { - ...value, - layout: { - ...value?.layout, - contentSize: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'layout', 'contentSize' ], newValue ) + ); }; const hasUserSetContentSizeValue = () => !! value?.layout?.contentSize; const resetContentSizeValue = () => setContentSizeValue( undefined ); @@ -239,13 +236,7 @@ export default function DimensionsPanel( { useHasWideSize( settings ) && includeLayoutControls; const wideSizeValue = decodeValue( inheritedValue?.layout?.wideSize ); const setWideSizeValue = ( newValue ) => { - onChange( { - ...value, - layout: { - ...value?.layout, - wideSize: newValue, - }, - } ); + onChange( immutableSet( value, [ 'layout', 'wideSize' ], newValue ) ); }; const hasUserSetWideSizeValue = () => !! value?.layout?.wideSize; const resetWideSizeValue = () => setWideSizeValue( undefined ); @@ -262,13 +253,7 @@ export default function DimensionsPanel( { paddingSides.some( ( side ) => AXIAL_SIDES.includes( side ) ); const setPaddingValues = ( newPaddingValues ) => { const padding = filterValuesBySides( newPaddingValues, paddingSides ); - onChange( { - ...value, - spacing: { - ...value?.spacing, - padding, - }, - } ); + onChange( immutableSet( value, [ 'spacing', 'padding' ], padding ) ); }; const hasPaddingValue = () => !! value?.spacing?.padding && @@ -288,13 +273,7 @@ export default function DimensionsPanel( { marginSides.some( ( side ) => AXIAL_SIDES.includes( side ) ); const setMarginValues = ( newMarginValues ) => { const margin = filterValuesBySides( newMarginValues, marginSides ); - onChange( { - ...value, - spacing: { - ...value?.spacing, - margin, - }, - } ); + onChange( immutableSet( value, [ 'spacing', 'margin' ], margin ) ); }; const hasMarginValue = () => !! value?.spacing?.margin && @@ -312,13 +291,9 @@ export default function DimensionsPanel( { const isAxialGap = gapSides && gapSides.some( ( side ) => AXIAL_SIDES.includes( side ) ); const setGapValue = ( newGapValue ) => { - onChange( { - ...value, - spacing: { - ...value?.spacing, - blockGap: newGapValue, - }, - } ); + onChange( + immutableSet( value, [ 'spacing', 'blockGap' ], newGapValue ) + ); }; const setGapValues = ( nextBoxGapValue ) => { if ( ! nextBoxGapValue ) { @@ -341,13 +316,9 @@ export default function DimensionsPanel( { const showMinHeightControl = useHasMinHeight( settings ); const minHeightValue = decodeValue( inheritedValue?.dimensions?.minHeight ); const setMinHeightValue = ( newValue ) => { - onChange( { - ...value, - dimensions: { - ...value?.dimensions, - minHeight: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'dimensions', 'minHeight' ], newValue ) + ); }; const resetMinHeightValue = () => { setMinHeightValue( undefined ); diff --git a/packages/block-editor/src/components/global-styles/get-block-css-selector.js b/packages/block-editor/src/components/global-styles/get-block-css-selector.js new file mode 100644 index 00000000000000..db58709fe79aae --- /dev/null +++ b/packages/block-editor/src/components/global-styles/get-block-css-selector.js @@ -0,0 +1,129 @@ +/** + * External dependencies + */ +import { get, isEmpty } from 'lodash'; + +/** + * Internal dependencies + */ +import { scopeSelector } from './utils'; + +/** + * Determine the CSS selector for the block type and target provided, returning + * it if available. + * + * @param {import('@wordpress/blocks').Block} blockType The block's type. + * @param {string|string[]} target The desired selector's target e.g. `root`, delimited string, or array path. + * @param {Object} options Options object. + * @param {boolean} options.fallback Whether or not to fallback to broader selector. + * + * @return {?string} The CSS selector or `null` if no selector available. + */ +export function getBlockCSSSelector( + blockType, + target = 'root', + options = {} +) { + if ( ! target ) { + return null; + } + + const { fallback = false } = options; + const { name, selectors, supports } = blockType; + + const hasSelectors = ! isEmpty( selectors ); + const path = Array.isArray( target ) ? target.join( '.' ) : target; + + // Duotone ( no fallback selectors for Duotone ). + if ( path === 'filter.duotone' ) { + // If selectors API in use, only use its value or null. + if ( hasSelectors ) { + return get( selectors, path, null ); + } + + // Selectors API, not available, check for old experimental selector. + return get( supports, 'color.__experimentalDuotone', null ); + } + + // Root selector. + + // Calculated before returning as it can be used as a fallback for feature + // selectors later on. + let rootSelector = null; + + if ( hasSelectors && selectors.root ) { + // Use the selectors API if available. + rootSelector = selectors?.root; + } else if ( supports?.__experimentalSelector ) { + // Use the old experimental selector supports property if set. + rootSelector = supports.__experimentalSelector; + } else { + // If no root selector found, generate default block class selector. + rootSelector = + '.wp-block-' + name.replace( 'core/', '' ).replace( '/', '-' ); + } + + // Return selector if it's the root target we are looking for. + if ( path === 'root' ) { + return rootSelector; + } + + // If target is not `root` or `duotone` we have a feature or subfeature + // as the target. If the target is a string convert to an array. + const pathArray = Array.isArray( target ) ? target : target.split( '.' ); + + // Feature selectors ( may fallback to root selector ); + if ( pathArray.length === 1 ) { + const fallbackSelector = fallback ? rootSelector : null; + + // Prefer the selectors API if available. + if ( hasSelectors ) { + // Get selector from either `feature.root` or shorthand path. + const featureSelector = + get( selectors, `${ path }.root`, null ) || + get( selectors, path, null ); + + // Return feature selector if found or any available fallback. + return featureSelector || fallbackSelector; + } + + // Try getting old experimental supports selector value. + const featureSelector = get( + supports, + `${ path }.__experimentalSelector`, + null + ); + + // If nothing to work with, provide fallback selector if available. + if ( ! featureSelector ) { + return fallbackSelector; + } + + // Scope the feature selector by the block's root selector. + return scopeSelector( rootSelector, featureSelector ); + } + + // Subfeature selector. + // This may fallback either to parent feature or root selector. + let subfeatureSelector; + + // Use selectors API if available. + if ( hasSelectors ) { + subfeatureSelector = get( selectors, path, null ); + } + + // Only return if we have a subfeature selector. + if ( subfeatureSelector ) { + return subfeatureSelector; + } + + // To this point we don't have a subfeature selector. If a fallback has been + // requested, remove subfeature from target path and return results of a + // call for the parent feature's selector. + if ( fallback ) { + return getBlockCSSSelector( blockType, pathArray[ 0 ], options ); + } + + // We tried. + return null; +} diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index c169cb03a60836..69ce0634847a9c 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -31,6 +31,8 @@ const VALID_SETTINGS = [ 'shadow.presets', 'shadow.defaultPresets', 'color.background', + 'color.button', + 'color.caption', 'color.custom', 'color.customDuotone', 'color.customGradient', @@ -39,6 +41,7 @@ const VALID_SETTINGS = [ 'color.defaultPalette', 'color.duotone', 'color.gradients', + 'color.heading', 'color.link', 'color.palette', 'color.text', @@ -251,6 +254,35 @@ export function useSettingsForBlockElement( }; } + updatedSettings.color = { + ...updatedSettings.color, + text: + updatedSettings.color?.text && + supportedStyles.includes( 'color' ), + background: + updatedSettings.color?.background && + ( supportedStyles.includes( 'background' ) || + supportedStyles.includes( 'backgroundColor' ) ), + button: + updatedSettings.color?.button && + supportedStyles.includes( 'buttonColor' ), + heading: + updatedSettings.color?.heading && + supportedStyles.includes( 'headingColor' ), + link: + updatedSettings.color?.link && + supportedStyles.includes( 'linkColor' ), + caption: + updatedSettings.color?.caption && + supportedStyles.includes( 'captionColor' ), + }; + + // Some blocks can enable background colors but disable gradients. + if ( ! supportedStyles.includes( 'background' ) ) { + updatedSettings.color.gradients = []; + updatedSettings.color.customGradient = false; + } + [ 'lineHeight', 'fontStyle', @@ -379,3 +411,51 @@ export function useColorsPerOrigin( settings ) { shouldDisplayDefaultColors, ] ); } + +export function useGradientsPerOrigin( settings ) { + const customGradients = settings?.color?.gradients?.custom; + const themeGradients = settings?.color?.gradients?.theme; + const defaultGradients = settings?.color?.gradients?.default; + const shouldDisplayDefaultGradients = settings?.color?.defaultGradients; + + return useMemo( () => { + const result = []; + if ( themeGradients && themeGradients.length ) { + result.push( { + name: _x( + 'Theme', + 'Indicates this palette comes from the theme.' + ), + gradients: themeGradients, + } ); + } + if ( + shouldDisplayDefaultGradients && + defaultGradients && + defaultGradients.length + ) { + result.push( { + name: _x( + 'Default', + 'Indicates this palette comes from WordPress.' + ), + gradients: defaultGradients, + } ); + } + if ( customGradients && customGradients.length ) { + result.push( { + name: _x( + 'Custom', + 'Indicates this palette is created by the user.' + ), + gradients: customGradients, + } ); + } + return result; + }, [ + customGradients, + themeGradients, + defaultGradients, + shouldDisplayDefaultGradients, + ] ); +} diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 372f56fcead0d4..97238de17a212c 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -3,8 +3,8 @@ export { useGlobalSetting, useGlobalStyle, useSettingsForBlockElement, - useColorsPerOrigin, } from './hooks'; +export { getBlockCSSSelector } from './get-block-css-selector'; export { useGlobalStylesOutput } from './use-global-styles-output'; export { GlobalStylesContext } from './context'; export { @@ -16,3 +16,4 @@ export { useHasDimensionsPanel, } from './dimensions-panel'; export { default as BorderPanel, useHasBorderPanel } from './border-panel'; +export { default as ColorPanel, useHasColorPanel } from './color-panel'; diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 1848906ed1fcde..a826c562044789 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -480,7 +480,7 @@ describe( 'global styles renderer', () => { expect( toStyles( tree, blockSelectors ) ).toEqual( 'body {margin: 0;}' + 'body{background-color: red;margin: 10px;padding: 10px;}a{color: blue;}a:hover{color: orange;}a:focus{color: orange;}h1{font-size: 42px;}.wp-block-group{margin-top: 10px;margin-right: 20px;margin-bottom: 30px;margin-left: 40px;padding-top: 11px;padding-right: 22px;padding-bottom: 33px;padding-left: 44px;}h1,h2,h3,h4,h5,h6{color: orange;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{color: hotpink;}h1 a:hover,h2 a:hover,h3 a:hover,h4 a:hover,h5 a:hover,h6 a:hover{color: red;}h1 a:focus,h2 a:focus,h3 a:focus,h4 a:focus,h5 a:focus,h6 a:focus{color: red;}' + - '.wp-block-image img, .wp-block-image .wp-crop-area{border-radius: 9999px }.wp-block-image{color: red;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + + '.wp-block-image img, .wp-block-image .wp-crop-area{border-radius: 9999px}.wp-block-image{color: red;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + '.has-white-color{color: var(--wp--preset--color--white) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}h1.has-blue-color,h2.has-blue-color,h3.has-blue-color,h4.has-blue-color,h5.has-blue-color,h6.has-blue-color{color: var(--wp--preset--color--blue) !important;}h1.has-blue-background-color,h2.has-blue-background-color,h3.has-blue-background-color,h4.has-blue-background-color,h5.has-blue-background-color,h6.has-blue-background-color{background-color: var(--wp--preset--color--blue) !important;}h1.has-blue-border-color,h2.has-blue-border-color,h3.has-blue-border-color,h4.has-blue-border-color,h5.has-blue-border-color,h6.has-blue-border-color{border-color: var(--wp--preset--color--blue) !important;}' ); } ); @@ -668,6 +668,34 @@ describe( 'global styles renderer', () => { describe( 'getBlockSelectors', () => { it( 'should return block selectors data', () => { + const imageSelectors = { + root: '.my-image', + border: '.my-image img, .my-image .crop-area', + filter: { duotone: 'img' }, + }; + const imageBlock = { + name: 'core/image', + selectors: imageSelectors, + }; + const blockTypes = [ imageBlock ]; + + expect( getBlockSelectors( blockTypes, () => {} ) ).toEqual( { + 'core/image': { + name: imageBlock.name, + selector: imageSelectors.root, + duotoneSelector: imageSelectors.filter.duotone, + fallbackGapValue: undefined, + featureSelectors: { + root: '.my-image', + border: '.my-image img, .my-image .crop-area', + filter: { duotone: 'img' }, + }, + hasLayoutSupport: false, + }, + } ); + } ); + + it( 'should return block selectors data with old experimental selectors', () => { const imageSupports = { __experimentalBorder: { radius: true, @@ -688,6 +716,7 @@ describe( 'global styles renderer', () => { duotoneSelector: imageSupports.color.__experimentalDuotone, fallbackGapValue: undefined, featureSelectors: { + root: '.my-image', border: '.my-image img, .my-image .crop-area', }, hasLayoutSupport: false, diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index 1c3c253c7b69db..cdaa6826ddd431 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -20,6 +20,7 @@ import LetterSpacingControl from '../letter-spacing-control'; import TextTransformControl from '../text-transform-control'; import TextDecorationControl from '../text-decoration-control'; import { getValueFromVariable } from './utils'; +import { immutableSet } from '../../utils/object'; const MIN_TEXT_COLUMNS = 1; const MAX_TEXT_COLUMNS = 6; @@ -161,15 +162,13 @@ export default function TypographyPanel( { const slug = fontFamilies?.find( ( { fontFamily: f } ) => f === newValue )?.slug; - onChange( { - ...value, - typography: { - ...value?.typography, - fontFamily: slug - ? `var:preset|font-family|${ slug }` - : newValue, - }, - } ); + onChange( + immutableSet( + value, + [ 'typography', 'fontFamily' ], + slug ? `var:preset|font-family|${ slug }` : newValue + ) + ); }; const hasFontFamily = () => !! value?.typography?.fontFamily; const resetFontFamily = () => setFontFamily( undefined ); @@ -188,13 +187,9 @@ export default function TypographyPanel( { ? `var:preset|font-size|${ metadata?.slug }` : newValue; - onChange( { - ...value, - typography: { - ...value?.typography, - fontSize: actualValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'fontSize' ], actualValue ) + ); }; const hasFontSize = () => !! value?.typography?.fontSize; const resetFontSize = () => setFontSize( undefined ); @@ -229,13 +224,9 @@ export default function TypographyPanel( { const hasLineHeightEnabled = useHasLineHeightControl( settings ); const lineHeight = decodeValue( inheritedValue?.typography?.lineHeight ); const setLineHeight = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - lineHeight: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'lineHeight' ], newValue ) + ); }; const hasLineHeight = () => !! value?.typography?.lineHeight; const resetLineHeight = () => setLineHeight( undefined ); @@ -246,13 +237,9 @@ export default function TypographyPanel( { inheritedValue?.typography?.letterSpacing ); const setLetterSpacing = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - letterSpacing: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'letterSpacing' ], newValue ) + ); }; const hasLetterSpacing = () => !! value?.typography?.letterSpacing; const resetLetterSpacing = () => setLetterSpacing( undefined ); @@ -261,13 +248,9 @@ export default function TypographyPanel( { const hasTextColumnsControl = useHasTextColumnsControl( settings ); const textColumns = decodeValue( inheritedValue?.typography?.textColumns ); const setTextColumns = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - textColumns: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'textColumns' ], newValue ) + ); }; const hasTextColumns = () => !! value?.typography?.textColumns; const resetTextColumns = () => setTextColumns( undefined ); @@ -278,13 +261,9 @@ export default function TypographyPanel( { inheritedValue?.typography?.textTransform ); const setTextTransform = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - textTransform: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'textTransform' ], newValue ) + ); }; const hasTextTransform = () => !! value?.typography?.textTransform; const resetTextTransform = () => setTextTransform( undefined ); @@ -295,13 +274,9 @@ export default function TypographyPanel( { inheritedValue?.typography?.textDecoration ); const setTextDecoration = ( newValue ) => { - onChange( { - ...value, - typography: { - ...value?.typography, - textDecoration: newValue, - }, - } ); + onChange( + immutableSet( value, [ 'typography', 'textDecoration' ], newValue ) + ); }; const hasTextDecoration = () => !! value?.typography?.textDecoration; const resetTextDecoration = () => setTextDecoration( undefined ); diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index cce2b61f0786e7..2f68312b76c6b3 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -13,13 +13,14 @@ import { store as blocksStore, } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; -import { useContext, useMemo } from '@wordpress/element'; +import { renderToString, useContext, useMemo } from '@wordpress/element'; import { getCSSRules } from '@wordpress/style-engine'; /** * Internal dependencies */ import { PRESET_METADATA, ROOT_BLOCK_SELECTOR, scopeSelector } from './utils'; +import { getBlockCSSSelector } from './get-block-css-selector'; import { getTypographyFontSizeValue } from './typography-utils'; import { GlobalStylesContext } from './context'; import { useGlobalSetting } from './hooks'; @@ -144,13 +145,16 @@ function getPresetsSvgFilters( blockPresets = {} ) { return [ 'default', 'theme' ] .filter( ( origin ) => presetByOrigin[ origin ] ) .flatMap( ( origin ) => - presetByOrigin[ origin ].map( ( preset ) => ( - - ) ) - ); + presetByOrigin[ origin ].map( ( preset ) => + renderToString( + + ) + ) + ) + .join( '' ); } ); } @@ -193,6 +197,89 @@ function concatFeatureVariationSelectorString( return combinedSelectors.join( ', ' ); } +/** + * Generate style declarations for a block's custom feature and subfeature + * selectors. + * + * NOTE: The passed `styles` object will be mutated by this function. + * + * @param {Object} selectors Custom selectors object for a block. + * @param {Object} styles A block's styles object. + * + * @return {Object} Style declarations. + */ +const getFeatureDeclarations = ( selectors, styles ) => { + const declarations = {}; + + Object.entries( selectors ).forEach( ( [ feature, selector ] ) => { + // We're only processing features/subfeatures that have styles. + if ( feature === 'root' || ! styles?.[ feature ] ) { + return; + } + + const isShorthand = typeof selector === 'string'; + + // If we have a selector object instead of shorthand process it. + if ( ! isShorthand ) { + Object.entries( selector ).forEach( + ( [ subfeature, subfeatureSelector ] ) => { + // Don't process root feature selector yet or any + // subfeature that doesn't have a style. + if ( + subfeature === 'root' || + ! styles?.[ feature ][ subfeature ] + ) { + return; + } + + // Create a temporary styles object and build + // declarations for subfeature. + const subfeatureStyles = { + [ feature ]: { + [ subfeature ]: styles[ feature ][ subfeature ], + }, + }; + const newDeclarations = + getStylesDeclarations( subfeatureStyles ); + + // Merge new declarations in with any others that + // share the same selector. + declarations[ subfeatureSelector ] = [ + ...( declarations[ subfeatureSelector ] || [] ), + ...newDeclarations, + ]; + + // Remove the subfeature's style now it will be + // included under its own selector not the block's. + delete styles[ feature ][ subfeature ]; + } + ); + } + + // Now subfeatures have been processed and removed, we can + // process root, or shorthand, feature selectors. + if ( isShorthand || selector.root ) { + const featureSelector = isShorthand ? selector : selector.root; + + // Create temporary style object and build declarations for feature. + const featureStyles = { [ feature ]: styles[ feature ] }; + const newDeclarations = getStylesDeclarations( featureStyles ); + + // Merge new declarations with any others that share the selector. + declarations[ featureSelector ] = [ + ...( declarations[ featureSelector ] || [] ), + ...newDeclarations, + ]; + + // Remove the feature from the block's styles now as it will be + // included under its own selector not the block's. + delete styles[ feature ]; + } + } ); + + return declarations; +}; + /** * Transform given style tree into a set of style declarations. * @@ -692,23 +779,16 @@ export const toStyles = ( // Process styles for block support features with custom feature level // CSS selectors set. if ( featureSelectors ) { - Object.entries( featureSelectors ).forEach( - ( [ featureName, featureSelector ] ) => { - if ( styles?.[ featureName ] ) { - const featureStyles = { - [ featureName ]: styles[ featureName ], - }; - const featureDeclarations = - getStylesDeclarations( featureStyles ); - delete styles[ featureName ]; - - if ( !! featureDeclarations.length ) { - ruleset = - ruleset + - `${ featureSelector }{${ featureDeclarations.join( - ';' - ) } }`; - } + const featureDeclarations = getFeatureDeclarations( + featureSelectors, + styles + ); + + Object.entries( featureDeclarations ).forEach( + ( [ cssSelector, declarations ] ) => { + if ( !! declarations.length ) { + const rules = declarations.join( ';' ); + ruleset = ruleset + `${ cssSelector }{${ rules }}`; } } ); @@ -720,43 +800,32 @@ export const toStyles = ( if ( styles?.variations?.[ styleVariationName ] ) { // If the block uses any custom selectors for block support, add those first. if ( featureSelectors ) { - Object.entries( featureSelectors ).forEach( - ( [ featureName, featureSelector ] ) => { - if ( - styles?.variations?.[ - styleVariationName - ]?.[ featureName ] - ) { - const featureStyles = { - [ featureName ]: - styles.variations[ - styleVariationName - ][ featureName ], - }; - const featureDeclarations = - getStylesDeclarations( - featureStyles + const featureDeclarations = + getFeatureDeclarations( + featureSelectors, + styles?.variations?.[ + styleVariationName + ] + ); + + Object.entries( featureDeclarations ).forEach( + ( [ baseSelector, declarations ] ) => { + if ( !! declarations.length ) { + const cssSelector = + concatFeatureVariationSelectorString( + baseSelector, + styleVariationSelector ); - delete styles.variations[ - styleVariationName - ][ featureName ]; - - if ( - !! featureDeclarations.length - ) { - ruleset = - ruleset + - `${ concatFeatureVariationSelectorString( - featureSelector, - styleVariationSelector - ) }{${ featureDeclarations.join( - ';' - ) } }`; - } + const rules = + declarations.join( ';' ); + ruleset = + ruleset + + `${ cssSelector }{${ rules }}`; } } ); } + // Otherwise add regular selectors. const styleVariationDeclarations = getStylesDeclarations( @@ -907,15 +976,37 @@ export function toSvgFilters( tree, blockSelectors ) { } ); } +const getSelectorsConfig = ( blockType, rootSelector ) => { + if ( ! isEmpty( blockType?.selectors ) ) { + return blockType.selectors; + } + + const config = { root: rootSelector }; + Object.entries( BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS ).forEach( + ( [ featureKey, featureName ] ) => { + const featureSelector = getBlockCSSSelector( + blockType, + featureKey + ); + + if ( featureSelector ) { + config[ featureName ] = featureSelector; + } + } + ); + + return config; +}; + export const getBlockSelectors = ( blockTypes, getBlockStyles ) => { const result = {}; blockTypes.forEach( ( blockType ) => { const name = blockType.name; - const selector = - blockType?.supports?.__experimentalSelector ?? - '.wp-block-' + name.replace( 'core/', '' ).replace( '/', '-' ); - const duotoneSelector = - blockType?.supports?.color?.__experimentalDuotone ?? null; + const selector = getBlockCSSSelector( blockType, 'root' ); + const duotoneSelector = getBlockCSSSelector( + blockType, + 'filter.duotone' + ); const hasLayoutSupport = !! blockType?.supports?.__experimentalLayout; const fallbackGapValue = blockType?.supports?.spacing?.blockGap?.__experimentalDefault; @@ -930,20 +1021,7 @@ export const getBlockSelectors = ( blockTypes, getBlockStyles ) => { } ); } // For each block support feature add any custom selectors. - const featureSelectors = {}; - Object.entries( BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS ).forEach( - ( [ featureKey, featureName ] ) => { - const featureSelector = - blockType?.supports?.[ featureKey ]?.__experimentalSelector; - - if ( featureSelector ) { - featureSelectors[ featureName ] = scopeSelector( - selector, - featureSelector - ); - } - } - ); + const featureSelectors = getSelectorsConfig( blockType, selector ); result[ name ] = { duotoneSelector, @@ -1049,9 +1127,9 @@ export function useGlobalStylesOutput() { hasFallbackGapSupport, disableLayoutStyles ); + const svgs = toSvgFilters( mergedConfig, blockSelectors ); - const filters = toSvgFilters( mergedConfig, blockSelectors ); - const stylesheets = [ + const styles = [ { css: customProperties, isGlobalStyles: true, @@ -1065,6 +1143,11 @@ export function useGlobalStylesOutput() { css: mergedConfig.styles.css ?? '', isGlobalStyles: true, }, + { + assets: svgs, + __unstableType: 'svg', + isGlobalStyles: true, + }, ]; // Loop through the blocks to check if there are custom CSS values. @@ -1073,7 +1156,7 @@ export function useGlobalStylesOutput() { getBlockTypes().forEach( ( blockType ) => { if ( mergedConfig.styles.blocks[ blockType.name ]?.css ) { const selector = blockSelectors[ blockType.name ].selector; - stylesheets.push( { + styles.push( { css: processCSSNesting( mergedConfig.styles.blocks[ blockType.name ]?.css, selector @@ -1083,7 +1166,7 @@ export function useGlobalStylesOutput() { } } ); - return [ stylesheets, mergedConfig.settings, filters ]; + return [ styles, mergedConfig.settings ]; }, [ hasBlockGapSupport, hasFallbackGapSupport, diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index e8ca27d4b97e4d..d1abd0a57dbc2d 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -16,7 +16,9 @@ export const ROOT_BLOCK_SUPPORTS = [ 'backgroundColor', 'color', 'linkColor', + 'captionColor', 'buttonColor', + 'headingColor', 'fontFamily', 'fontSize', 'fontStyle', @@ -103,6 +105,7 @@ export const STYLE_PATH_TO_CSS_VAR_INFIX = { 'elements.link.typography.fontSize': 'font-size', 'elements.button.color.text': 'color', 'elements.button.color.background': 'color', + 'elements.caption.color.text': 'color', 'elements.button.typography.fontFamily': 'font-family', 'elements.button.typography.fontSize': 'font-size', 'elements.heading.color': 'color', diff --git a/packages/block-editor/src/components/image-size-control/index.js b/packages/block-editor/src/components/image-size-control/index.js index db2ad3c3de14f5..46e87de60f2fc8 100644 --- a/packages/block-editor/src/components/image-size-control/index.js +++ b/packages/block-editor/src/components/image-size-control/index.js @@ -38,17 +38,16 @@ export default function ImageSizeControl( { { imageSizeOptions && imageSizeOptions.length > 0 && ( ) } { isResizable && (
-

{ __( 'Image dimensions' ) }

- updateDimension( 'width', value ) } + size="__unstable-large" /> updateDimension( 'height', value ) } + size="__unstable-large" /> diff --git a/packages/block-editor/src/components/image-size-control/test/index.js b/packages/block-editor/src/components/image-size-control/test/index.js index 3033e07147e394..c36be27971e694 100644 --- a/packages/block-editor/src/components/image-size-control/test/index.js +++ b/packages/block-editor/src/components/image-size-control/test/index.js @@ -334,7 +334,7 @@ describe( 'ImageSizeControl', () => { ); expect( - screen.getByRole( 'combobox', { name: 'Image size' } ) + screen.getByRole( 'combobox', { name: 'Resolution' } ) ).toHaveValue( 'medium' ); } ); @@ -351,7 +351,7 @@ describe( 'ImageSizeControl', () => { ); await user.selectOptions( - screen.getByRole( 'combobox', { name: 'Image size' } ), + screen.getByRole( 'combobox', { name: 'Resolution' } ), 'thumbnail' ); diff --git a/packages/block-editor/src/components/inner-blocks/README.md b/packages/block-editor/src/components/inner-blocks/README.md index ce4eacb5e09954..eb42da998f0d0c 100644 --- a/packages/block-editor/src/components/inner-blocks/README.md +++ b/packages/block-editor/src/components/inner-blocks/README.md @@ -130,7 +130,7 @@ _Options:_ - `'insert'` — prevents inserting or removing blocks, but allows moving existing ones. - `false` — prevents locking from being applied to an `InnerBlocks` area even if a parent block contains locking. ( Boolean ) -If locking is not set in an `InnerBlocks` area: the locking of the parent `InnerBlocks` area is used. Note that `contentOnly` can't be overriden: it's present, the `templateLock` value of any children is ignored. +If locking is not set in an `InnerBlocks` area: the locking of the parent `InnerBlocks` area is used. Note that `contentOnly` can't be overridden: it's present, the `templateLock` value of any children is ignored. If the block is a top level block: the locking of the Custom Post Type is used. diff --git a/packages/block-editor/src/components/inspector-controls-tabs/position-controls-panel.js b/packages/block-editor/src/components/inspector-controls-tabs/position-controls-panel.js index f145d961e1f303..9a3670b5deb286 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/position-controls-panel.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/position-controls-panel.js @@ -5,6 +5,8 @@ import { PanelBody, __experimentalUseSlotFills as useSlotFills, } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useLayoutEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -12,26 +14,55 @@ import { __ } from '@wordpress/i18n'; */ import InspectorControlsGroups from '../inspector-controls/groups'; import { default as InspectorControls } from '../inspector-controls'; +import { store as blockEditorStore } from '../../store'; -const PositionControls = () => { - const fills = useSlotFills( - InspectorControlsGroups.position.Slot.__unstableName - ); - const hasFills = Boolean( fills && fills.length ); +const PositionControlsPanel = () => { + const [ initialOpen, setInitialOpen ] = useState(); - if ( ! hasFills ) { - return null; - } + // Determine whether the panel should be expanded. + const { multiSelectedBlocks } = useSelect( ( select ) => { + const { getBlocksByClientId, getSelectedBlockClientIds } = + select( blockEditorStore ); + const clientIds = getSelectedBlockClientIds(); + return { + multiSelectedBlocks: getBlocksByClientId( clientIds ), + }; + }, [] ); + + useLayoutEffect( () => { + // If any selected block has a position set, open the panel by default. + // The first block's value will still be used within the control though. + if ( initialOpen === undefined ) { + setInitialOpen( + multiSelectedBlocks.some( + ( { attributes } ) => !! attributes?.style?.position?.type + ) + ); + } + }, [ initialOpen, multiSelectedBlocks, setInitialOpen ] ); return ( ); }; +const PositionControls = () => { + const fills = useSlotFills( + InspectorControlsGroups.position.Slot.__unstableName + ); + const hasFills = Boolean( fills && fills.length ); + + if ( ! hasFills ) { + return null; + } + + return ; +}; + export default PositionControls; diff --git a/packages/block-editor/src/components/line-height-control/index.js b/packages/block-editor/src/components/line-height-control/index.js index 670d6fac37e8df..61e0c4a3f32893 100644 --- a/packages/block-editor/src/components/line-height-control/index.js +++ b/packages/block-editor/src/components/line-height-control/index.js @@ -84,6 +84,15 @@ const LineHeightControl = ( { ? undefined : { marginBottom: 24 }; + const handleOnChange = ( nextValue, { event } ) => { + if ( event.type === 'click' ) { + onChange( adjustNextValue( nextValue, false ) ); + return; + } + + onChange( nextValue ); + }; + return (
{ + const [ insertedBlock, setInsertedBlock ] = useState( null ); + + const instanceId = useInstanceId( Appender ); + const { hideInserter } = useSelect( + ( select ) => { + const { getTemplateLock, __unstableGetEditorMode } = + select( blockEditorStore ); + + return { + hideInserter: + !! getTemplateLock( clientId ) || + __unstableGetEditorMode() === 'zoom-out', + }; + }, + [ clientId ] + ); + + const blockTitle = useBlockDisplayTitle( { + clientId, + context: 'list-view', + } ); + + const insertedBlockTitle = useBlockDisplayTitle( { + clientId: insertedBlock?.clientId, + context: 'list-view', + } ); + + useEffect( () => { + if ( ! insertedBlockTitle?.length ) { + return; + } + + speak( + sprintf( + // translators: %s: name of block being inserted (i.e. Paragraph, Image, Group etc) + __( '%s block inserted' ), + insertedBlockTitle + ), + 'assertive' + ); + }, [ insertedBlockTitle ] ); + + if ( hideInserter ) { + return null; + } + + const descriptionId = `list-view-appender__${ instanceId }`; + const description = sprintf( + /* translators: 1: The name of the block. 2: The numerical position of the block. 3: The level of nesting for the block. */ + __( 'Append to %1$s block at position %2$d, Level %3$d' ), + blockTitle, + blockCount + 1, + nestingLevel + ); + + return ( +
+ { + if ( maybeInsertedBlock?.clientId ) { + setInsertedBlock( maybeInsertedBlock ); + } + } } + /> +
+ { description } +
+
+ ); + } +); diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 7034bb92ecffd5..976323eb195e7c 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -33,7 +33,6 @@ import { BlockMoverDownButton, } from '../block-mover/button'; import ListViewBlockContents from './block-contents'; -import BlockSettingsDropdown from '../block-settings-menu/block-settings-dropdown'; import { useListViewContext } from './context'; import { getBlockPositionDescription } from './utils'; import { store as blockEditorStore } from '../../store'; @@ -135,7 +134,8 @@ function ListViewBlock( { ) : __( 'Options' ); - const { isTreeGridMounted, expand, collapse } = useListViewContext(); + const { isTreeGridMounted, expand, collapse, BlockSettingsMenu } = + useListViewContext(); const hasSiblings = siblingBlockCount > 0; const hasRenderedMovers = showBlockMovers && hasSiblings; @@ -321,14 +321,15 @@ function ListViewBlock( { ) } - { showBlockActions && ( + { showBlockActions && BlockSettingsMenu && ( { ( { ref, tabIndex, onFocus } ) => ( - ); } ) } + { showAppender && ( + + + { ( treeGridCellProps ) => ( + + ) } + + + ) } ); } diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 5425290809a18b..aff471e18fa1ae 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -28,6 +28,7 @@ import useListViewClientIds from './use-list-view-client-ids'; import useListViewDropZone from './use-list-view-drop-zone'; import useListViewExpandSelectedItem from './use-list-view-expand-selected-item'; import { store as blockEditorStore } from '../../store'; +import { BlockSettingsDropdown } from '../block-settings-menu/block-settings-dropdown'; const expanded = ( state, action ) => { if ( Array.isArray( action.clientIds ) ) { @@ -47,18 +48,30 @@ const expanded = ( state, action ) => { export const BLOCK_LIST_ITEM_HEIGHT = 36; +/** @typedef {import('react').ComponentType} ComponentType */ +/** @typedef {import('react').Ref} Ref */ + /** * Show a hierarchical list of blocks. * - * @param {Object} props Components props. - * @param {string} props.id An HTML element id for the root element of ListView. - * @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy. - * @param {boolean} props.showBlockMovers Flag to enable block movers - * @param {boolean} props.isExpanded Flag to determine whether nested levels are expanded by default. - * @param {Object} ref Forwarded ref + * @param {Object} props Components props. + * @param {string} props.id An HTML element id for the root element of ListView. + * @param {Array} props.blocks Custom subset of block client IDs to be used instead of the default hierarchy. + * @param {?boolean} props.showBlockMovers Flag to enable block movers. Defaults to `false`. + * @param {?boolean} props.isExpanded Flag to determine whether nested levels are expanded by default. Defaults to `false`. + * @param {?boolean} props.showAppender Flag to show or hide the block appender. Defaults to `false`. + * @param {?ComponentType} props.blockSettingsMenu Optional more menu substitution. Defaults to the standard `BlockSettingsDropdown` component. + * @param {Ref} ref Forwarded ref */ -function ListView( - { id, blocks, showBlockMovers = false, isExpanded = false }, +function ListViewComponent( + { + id, + blocks, + showBlockMovers = false, + isExpanded = false, + showAppender = false, + blockSettingsMenu: BlockSettingsMenu = BlockSettingsDropdown, + }, ref ) { const { clientIdsTree, draggedClientIds, selectedClientIds } = @@ -170,8 +183,16 @@ function ListView( expandedState, expand, collapse, + BlockSettingsMenu, } ), - [ isMounted.current, draggedClientIds, expandedState, expand, collapse ] + [ + isMounted.current, + draggedClientIds, + expandedState, + expand, + collapse, + BlockSettingsMenu, + ] ); // If there are no blocks to show, do not render the list view. @@ -204,10 +225,22 @@ function ListView( selectedClientIds={ selectedClientIds } isExpanded={ isExpanded } shouldShowInnerBlocks={ shouldShowInnerBlocks } + showAppender={ showAppender } /> ); } -export default forwardRef( ListView ); +export const PrivateListView = forwardRef( ListViewComponent ); + +export default forwardRef( ( props, ref ) => { + return ( + + ); +} ); diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 0bedb39061af4c..9a1b2f501d4dca 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -410,3 +410,22 @@ $block-navigation-max-indent: 8; height: 36px; } +.list-view-appender .block-editor-inserter__toggle { + background-color: #1e1e1e; + color: #fff; + margin: $grid-unit-10 0 0 24px; + border-radius: 2px; + height: 24px; + min-width: 24px; + padding: 0; + + &:hover, + &:focus { + background: var(--wp-admin-theme-color); + color: #fff; + } +} + +.list-view-appender__description { + display: none; +} diff --git a/packages/block-editor/src/components/media-replace-flow/index.js b/packages/block-editor/src/components/media-replace-flow/index.js index a2fb5f36036aaa..29790419af1317 100644 --- a/packages/block-editor/src/components/media-replace-flow/index.js +++ b/packages/block-editor/src/components/media-replace-flow/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -58,6 +63,7 @@ const MediaReplaceFlow = ( { const mediaUpload = useSelect( ( select ) => { return select( blockEditorStore ).getSettings().mediaUpload; }, [] ); + const canUpload = !! mediaUpload; const editMediaButtonRef = useRef(); const errorNoticeID = `block-editor/media-replace-flow/error-notice/${ ++uniqueId }`; @@ -152,7 +158,7 @@ const MediaReplaceFlow = ( { renderContent={ ( { onClose } ) => ( <> - <> + ) } /> - - { - uploadFiles( event, onClose ); - } } - accept={ accept } - multiple={ multiple } - render={ ( { openFileDialog } ) => { - return ( - { - openFileDialog(); - } } - > - { __( 'Upload' ) } - - ); - } } - /> - - + { + uploadFiles( event, onClose ); + } } + accept={ accept } + multiple={ multiple } + render={ ( { openFileDialog } ) => { + return ( + { + openFileDialog(); + } } + > + { __( 'Upload' ) } + + ); + } } + /> + { onToggleFeaturedImage && ( { onSelectURL && ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -
+ { __( 'Current media URL:' ) } diff --git a/packages/block-editor/src/components/media-replace-flow/style.scss b/packages/block-editor/src/components/media-replace-flow/style.scss index 81484cbf806623..dd3b0563c3ca8a 100644 --- a/packages/block-editor/src/components/media-replace-flow/style.scss +++ b/packages/block-editor/src/components/media-replace-flow/style.scss @@ -10,12 +10,15 @@ } .block-editor-media-flow__url-input { - border-top: $border-width solid $gray-900; - margin-top: $grid-unit-10; margin-right: -$grid-unit-10; margin-left: -$grid-unit-10; padding: $grid-unit-20; + &.has-siblings { + border-top: $border-width solid $gray-900; + margin-top: $grid-unit-10; + } + .block-editor-media-replace-flow__image-url-label { display: block; top: $grid-unit-20; diff --git a/packages/block-editor/src/components/rich-text/format-edit.js b/packages/block-editor/src/components/rich-text/format-edit.js index 3ab3f47d226aec..75b077ab321d43 100644 --- a/packages/block-editor/src/components/rich-text/format-edit.js +++ b/packages/block-editor/src/components/rich-text/format-edit.js @@ -1,11 +1,7 @@ /** * WordPress dependencies */ -import { - getActiveFormat, - getActiveObject, - isCollapsed, -} from '@wordpress/rich-text'; +import { getActiveFormat, getActiveObject } from '@wordpress/rich-text'; export default function FormatEdit( { formatTypes, @@ -22,37 +18,11 @@ export default function FormatEdit( { } const activeFormat = getActiveFormat( value, name ); - let isActive = activeFormat !== undefined; + const isActive = activeFormat !== undefined; const activeObject = getActiveObject( value ); const isObjectActive = activeObject !== undefined && activeObject.type === name; - // Edge case: un-collapsed link formats. - // If there is a missing link format at either end of the selection - // then we shouldn't show the Edit UI because the selection has exceeded - // the bounds of the link format. - // Also if the format objects don't match then we're dealing with two separate - // links so we should not allow the link to be modified over the top. - if ( name === 'core/link' && ! isCollapsed( value ) ) { - const formats = value.formats; - - const linkFormatAtStart = formats[ value.start ]?.find( - ( { type } ) => type === 'core/link' - ); - - const linkFormatAtEnd = formats[ value.end - 1 ]?.find( - ( { type } ) => type === 'core/link' - ); - - if ( - ! linkFormatAtStart || - ! linkFormatAtEnd || - linkFormatAtStart !== linkFormatAtEnd - ) { - isActive = false; - } - } - return ( { function onBeforeInput( event ) { - if ( ! hasMultiSelection() ) { - return; - } - // Prevent the browser to format something when we have multiselection. - if ( event.inputType?.startsWith( 'format' ) ) { + // If writing flow is editable, NEVER allow the browser to alter the + // DOM. This will cause React errors (and the DOM should only be + // altered in a controlled fashion). + if ( node.contentEditable === 'true' ) { event.preventDefault(); } } diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index a5f08b64515b01..de6aa349898645 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -163,7 +163,6 @@ export function BorderPanel( props ) { { return colorSupport && colorSupport.text !== false; }; -/** - * Clears a single color property from a style object. - * - * @param {Array} path Path to color property to clear within styles object. - * @param {Object} style Block attributes style object. - * @return {Object} Styles with the color property omitted. - */ -const clearColorFromStyles = ( path, style ) => - cleanEmptyObject( immutableSet( style, path, undefined ) ); - -/** - * Clears text color related properties from supplied attributes. - * - * @param {Object} attributes Block attributes. - * @return {Object} Update block attributes with text color properties omitted. - */ -const resetAllTextFilter = ( attributes ) => ( { - textColor: undefined, - style: clearColorFromStyles( [ 'color', 'text' ], attributes.style ), -} ); - -/** - * Clears link color related properties from supplied attributes. - * - * @param {Object} attributes Block attributes. - * @return {Object} Update block attributes with link color properties omitted. - */ -const resetAllLinkFilter = ( attributes ) => ( { - style: clearColorFromStyles( - [ 'elements', 'link', 'color', 'text' ], - attributes.style - ), -} ); - -/** - * Clears all background color related properties including gradients from - * supplied block attributes. - * - * @param {Object} attributes Block attributes. - * @return {Object} Block attributes with background and gradient omitted. - */ -const clearBackgroundAndGradient = ( attributes ) => ( { - backgroundColor: undefined, - gradient: undefined, - style: { - ...attributes.style, - color: { - ...attributes.style?.color, - background: undefined, - gradient: undefined, - }, - }, -} ); - /** * Filters registered block settings, extending attributes to include * `backgroundColor` and `textColor` attribute. @@ -273,194 +218,112 @@ export function addEditProps( settings ) { return settings; } -const getLinkColorFromAttributeValue = ( colors, value ) => { - const attributeParsed = /var:preset\|color\|(.+)/.exec( value ); - if ( attributeParsed && attributeParsed[ 1 ] ) { - return getColorObjectByAttributeValues( colors, attributeParsed[ 1 ] ) - .color; - } - return value; -}; - -/** - * Inspector control panel containing the color related configuration - * - * @param {Object} props - * - * @return {WPElement} Color edit element. - */ -export function ColorEdit( props ) { - const { name: blockName, attributes } = props; - // Some color settings have a special handling for deprecated flags in `useSetting`, - // so we can't unwrap them by doing const { ... } = useSetting('color') - // until https://github.com/WordPress/gutenberg/issues/37094 is fixed. - const userPalette = useSetting( 'color.palette.custom' ); - const themePalette = useSetting( 'color.palette.theme' ); - const defaultPalette = useSetting( 'color.palette.default' ); - const allSolids = useMemo( - () => [ - ...( userPalette || [] ), - ...( themePalette || [] ), - ...( defaultPalette || [] ), - ], - [ userPalette, themePalette, defaultPalette ] - ); - const userGradientPalette = useSetting( 'color.gradients.custom' ); - const themeGradientPalette = useSetting( 'color.gradients.theme' ); - const defaultGradientPalette = useSetting( 'color.gradients.default' ); - const allGradients = useMemo( - () => [ - ...( userGradientPalette || [] ), - ...( themeGradientPalette || [] ), - ...( defaultGradientPalette || [] ), - ], - [ userGradientPalette, themeGradientPalette, defaultGradientPalette ] - ); - const areCustomSolidsEnabled = useSetting( 'color.custom' ); - const areCustomGradientsEnabled = useSetting( 'color.customGradient' ); - const isBackgroundEnabled = useSetting( 'color.background' ); - const isLinkEnabled = useSetting( 'color.link' ); - const isTextEnabled = useSetting( 'color.text' ); - - const solidsEnabled = - areCustomSolidsEnabled || ! themePalette || themePalette?.length > 0; - - const gradientsEnabled = - areCustomGradientsEnabled || - ! themeGradientPalette || - themeGradientPalette?.length > 0; - - // Shouldn't be needed but right now the ColorGradientsPanel - // can trigger both onChangeColor and onChangeBackground - // synchronously causing our two callbacks to override changes - // from each other. - const localAttributes = useRef( attributes ); - useEffect( () => { - localAttributes.current = attributes; - }, [ attributes ] ); - - if ( ! hasColorSupport( blockName ) ) { - return null; - } - - const hasLinkColor = - hasLinkColorSupport( blockName ) && isLinkEnabled && solidsEnabled; - const hasTextColor = - hasTextColorSupport( blockName ) && isTextEnabled && solidsEnabled; - const hasBackgroundColor = - hasBackgroundColorSupport( blockName ) && - isBackgroundEnabled && - solidsEnabled; - const hasGradientColor = - hasGradientSupport( blockName ) && gradientsEnabled; - - if ( - ! hasLinkColor && - ! hasTextColor && - ! hasBackgroundColor && - ! hasGradientColor - ) { - return null; - } +function styleToAttributes( style ) { + const textColorValue = style?.color?.text; + const textColorSlug = textColorValue?.startsWith( 'var:preset|color|' ) + ? textColorValue.substring( 'var:preset|color|'.length ) + : undefined; + const backgroundColorValue = style?.color?.background; + const backgroundColorSlug = backgroundColorValue?.startsWith( + 'var:preset|color|' + ) + ? backgroundColorValue.substring( 'var:preset|color|'.length ) + : undefined; + const gradientValue = style?.color?.gradient; + const gradientSlug = gradientValue?.startsWith( 'var:preset|gradient|' ) + ? gradientValue.substring( 'var:preset|gradient|'.length ) + : undefined; + const updatedStyle = { ...style }; + updatedStyle.color = { + ...updatedStyle.color, + text: textColorSlug ? undefined : textColorValue, + background: backgroundColorSlug ? undefined : backgroundColorValue, + gradient: gradientSlug ? undefined : gradientValue, + }; + return { + style: cleanEmptyObject( updatedStyle ), + textColor: textColorSlug, + backgroundColor: backgroundColorSlug, + gradient: gradientSlug, + }; +} - const { style, textColor, backgroundColor, gradient } = attributes; - let gradientValue; - if ( hasGradientColor && gradient ) { - gradientValue = getGradientValueBySlug( allGradients, gradient ); - } else if ( hasGradientColor ) { - gradientValue = style?.color?.gradient; - } +function attributesToStyle( attributes ) { + return { + ...attributes.style, + color: { + ...attributes.style?.color, + text: attributes.textColor + ? 'var:preset|color|' + attributes.textColor + : attributes.style?.color?.text, + background: attributes.backgroundColor + ? 'var:preset|color|' + attributes.backgroundColor + : attributes.style?.color?.background, + gradient: attributes.gradient + ? 'var:preset|gradient|' + attributes.gradient + : attributes.style?.color?.gradient, + }, + }; +} - const onChangeColor = ( name ) => ( value ) => { - const colorObject = getColorObjectByColorValue( allSolids, value ); - const attributeName = name + 'Color'; - const newStyle = { - ...localAttributes.current.style, - color: { - ...localAttributes.current?.style?.color, - [ name ]: colorObject?.slug ? undefined : value, - }, - }; +function ColorInspectorControl( { children, resetAllFilter } ) { + const attributesResetAllFilter = useCallback( + ( attributes ) => { + const existingStyle = attributesToStyle( attributes ); + const updatedStyle = resetAllFilter( existingStyle ); + return { + ...attributes, + ...styleToAttributes( updatedStyle ), + }; + }, + [ resetAllFilter ] + ); - const newNamedColor = colorObject?.slug ? colorObject.slug : undefined; - const newAttributes = { - style: cleanEmptyObject( newStyle ), - [ attributeName ]: newNamedColor, - }; + return ( + + { children } + + ); +} - props.setAttributes( newAttributes ); - localAttributes.current = { - ...localAttributes.current, - ...newAttributes, - }; - }; +export function ColorEdit( props ) { + const { clientId, name, attributes, setAttributes } = props; + const settings = useBlockSettings( name ); + const isEnabled = useHasColorPanel( settings ); + const value = useMemo( () => { + return attributesToStyle( { + style: attributes.style, + textColor: attributes.textColor, + backgroundColor: attributes.backgroundColor, + gradient: attributes.gradient, + } ); + }, [ + attributes.style, + attributes.textColor, + attributes.backgroundColor, + attributes.gradient, + ] ); - const onChangeGradient = ( value ) => { - const slug = getGradientSlugByValue( allGradients, value ); - let newAttributes; - if ( slug ) { - const newStyle = { - ...localAttributes.current?.style, - color: { - ...localAttributes.current?.style?.color, - gradient: undefined, - }, - }; - newAttributes = { - style: cleanEmptyObject( newStyle ), - gradient: slug, - }; - } else { - const newStyle = { - ...localAttributes.current?.style, - color: { - ...localAttributes.current?.style?.color, - gradient: value, - }, - }; - newAttributes = { - style: cleanEmptyObject( newStyle ), - gradient: undefined, - }; - } - props.setAttributes( newAttributes ); - localAttributes.current = { - ...localAttributes.current, - ...newAttributes, - }; + const onChange = ( newStyle ) => { + setAttributes( styleToAttributes( newStyle ) ); }; - const onChangeLinkColor = ( value ) => { - const colorObject = getColorObjectByColorValue( allSolids, value ); - const newLinkColorValue = colorObject?.slug - ? `var:preset|color|${ colorObject.slug }` - : value; - - const newStyle = cleanEmptyObject( - immutableSet( - localAttributes.current?.style, - [ 'elements', 'link', 'color', 'text' ], - newLinkColorValue - ) - ); - props.setAttributes( { style: newStyle } ); - localAttributes.current = { - ...localAttributes.current, - ...{ style: newStyle }, - }; - }; + if ( ! isEnabled ) { + return null; + } - const defaultColorControls = getBlockSupport( props.name, [ + const defaultControls = getBlockSupport( props.name, [ COLOR_SUPPORT_KEY, '__experimentalDefaultControls', ] ); const enableContrastChecking = Platform.OS === 'web' && - ! gradient && - ! style?.color?.gradient && - hasBackgroundColor && - ( hasLinkColor || hasTextColor ) && + ! value?.color?.gradient && + ( settings?.color?.text || settings?.color?.link ) && // Contrast checking is enabled by default. // Deactivating it requires `enableContrastChecker` to have // an explicit value of `false`. @@ -471,64 +334,25 @@ export function ColorEdit( props ) { ] ); return ( - + + { enableContrastChecking && ( + + ) } + ); } diff --git a/packages/block-editor/src/hooks/content-lock-ui.js b/packages/block-editor/src/hooks/content-lock-ui.js index 1fa8ffc29c8586..568da7974925a1 100644 --- a/packages/block-editor/src/hooks/content-lock-ui.js +++ b/packages/block-editor/src/hooks/content-lock-ui.js @@ -104,9 +104,13 @@ export const withBlockControls = createHigherOrderComponent( return ; } + const showStopEditingAsBlocks = isEditingAsBlocks && ! isContentLocked; + const showStartEditingAsBlocks = + ! isEditingAsBlocks && isContentLocked && props.isSelected; + return ( <> - { isEditingAsBlocks && ! isContentLocked && ( + { showStopEditingAsBlocks && ( <> ) } - { ! isEditingAsBlocks && isContentLocked && props.isSelected && ( + { showStartEditingAsBlocks && ( { ( { onClose } ) => ( setting?.colorValue ); + // There are so many things that can change the color of a block + // So we perform this check on every render. + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect( () => { - if ( ! enableContrastChecking ) { - return; - } - if ( ! definedColors.length ) { - if ( detectedBackgroundColor ) { - setDetectedBackgroundColor(); - } - if ( detectedColor ) { - setDetectedColor(); - } - if ( detectedLinkColor ) { - setDetectedColor(); - } - return; - } - if ( ! ref.current ) { return; } @@ -72,26 +50,12 @@ export default function ColorPanel( { setDetectedBackgroundColor( backgroundColor ); } ); - const colorGradientSettings = useMultipleOriginColorsAndGradients(); - return ( - - - { enableContrastChecking && ( - - ) } - + ); } diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 046800bf0144de..02c3931ef9850d 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -105,7 +105,6 @@ export function DimensionsPanel( props ) { { - const rawElementsStyles = props.attributes.style?.elements; + // The .editor-styles-wrapper selector is required on elements styles. As it is + // added to all other editor styles, not providing it causes reset and global + // styles to override element styles because of higher specificity. + const elements = [ + { + styles: ! skipLinkColorSerialization + ? props.attributes.style?.elements?.link + : undefined, + selector: `.editor-styles-wrapper .${ blockElementsContainerIdentifier } ${ ELEMENTS.link }`, + }, + { + styles: ! skipLinkColorSerialization + ? props.attributes.style?.elements?.link?.[ ':hover' ] + : undefined, + selector: `.editor-styles-wrapper .${ blockElementsContainerIdentifier } ${ ELEMENTS.link }:hover`, + }, + ]; const elementCssRules = []; - if ( - rawElementsStyles && - Object.keys( rawElementsStyles ).length > 0 - ) { - // Remove values based on whether serialization has been skipped for a specific style. - const filteredElementsStyles = { - ...rawElementsStyles, - link: { - ...rawElementsStyles.link, - color: ! skipLinkColorSerialization - ? rawElementsStyles.link?.color - : undefined, - }, - }; - - for ( const [ elementName, elementStyles ] of Object.entries( - filteredElementsStyles - ) ) { + for ( const { styles: elementStyles, selector } of elements ) { + if ( elementStyles ) { const cssRule = compileCSS( elementStyles, { - // The .editor-styles-wrapper selector is required on elements styles. As it is - // added to all other editor styles, not providing it causes reset and global - // styles to override element styles because of higher specificity. - selector: `.editor-styles-wrapper .${ blockElementsContainerIdentifier } ${ ELEMENTS[ elementName ] }`, + selector, } ); - if ( !! cssRule ) { - elementCssRules.push( cssRule ); - } + elementCssRules.push( cssRule ); } } - return elementCssRules.length > 0 ? elementCssRules : undefined; - }, [ props.attributes.style?.elements ] ); + return elementCssRules.length > 0 + ? elementCssRules.join( '' ) + : undefined; + }, [ + props.attributes.style?.elements, + blockElementsContainerIdentifier, + skipLinkColorSerialization, + ] ); const element = useContext( BlockList.__unstableElementContext ); diff --git a/packages/block-editor/src/hooks/test/utils.js b/packages/block-editor/src/hooks/test/utils.js index bfbced3195b7fb..a919fad575312e 100644 --- a/packages/block-editor/src/hooks/test/utils.js +++ b/packages/block-editor/src/hooks/test/utils.js @@ -7,113 +7,9 @@ import { applyFilters } from '@wordpress/hooks'; * Internal dependencies */ import '../anchor'; -import { immutableSet } from '../utils'; const noop = () => {}; -describe( 'immutableSet', () => { - describe( 'handling falsy values properly', () => { - it( 'should create a new object if `undefined` is passed', () => { - const result = immutableSet( undefined, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if `null` is passed', () => { - const result = immutableSet( null, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if `false` is passed', () => { - const result = immutableSet( false, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if `0` is passed', () => { - const result = immutableSet( 0, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if an empty string is passed', () => { - const result = immutableSet( '', 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'should create a new object if a NaN is passed', () => { - const result = immutableSet( NaN, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - } ); - - describe( 'manages data assignment properly', () => { - it( 'assigns value properly when it does not exist', () => { - const result = immutableSet( {}, 'test', 1 ); - - expect( result ).toEqual( { test: 1 } ); - } ); - - it( 'overrides existing values', () => { - const result = immutableSet( { test: 1 }, 'test', 2 ); - - expect( result ).toEqual( { test: 2 } ); - } ); - - describe( 'with array notation access', () => { - it( 'assigns values at deeper levels', () => { - const result = immutableSet( {}, [ 'foo', 'bar', 'baz' ], 5 ); - - expect( result ).toEqual( { foo: { bar: { baz: 5 } } } ); - } ); - - it( 'overrides existing values at deeper levels', () => { - const result = immutableSet( - { foo: { bar: { baz: 1 } } }, - [ 'foo', 'bar', 'baz' ], - 5 - ); - - expect( result ).toEqual( { foo: { bar: { baz: 5 } } } ); - } ); - - it( 'keeps other properties intact', () => { - const result = immutableSet( - { foo: { bar: { baz: 1 } } }, - [ 'foo', 'bar', 'test' ], - 5 - ); - - expect( result ).toEqual( { - foo: { bar: { baz: 1, test: 5 } }, - } ); - } ); - } ); - } ); - - describe( 'does not mutate the original object', () => { - it( 'clones the object at the first level', () => { - const input = {}; - const result = immutableSet( input, 'test', 1 ); - - expect( result ).not.toBe( input ); - } ); - - it( 'clones the object at deeper levels', () => { - const input = { foo: { bar: { baz: 1 } } }; - const result = immutableSet( input, [ 'foo', 'bar', 'baz' ], 2 ); - - expect( result ).not.toBe( input ); - expect( result.foo ).not.toBe( input.foo ); - expect( result.foo.bar ).not.toBe( input.foo.bar ); - expect( result.foo.bar.baz ).not.toBe( input.foo.bar.baz ); - } ); - } ); -} ); - describe( 'anchor', () => { const blockSettings = { save: noop, diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index 00e1c348f57c80..cb98c4098c4774 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -138,7 +138,6 @@ export function TypographyPanel( { { return isEmpty( cleanedNestedObjects ) ? undefined : cleanedNestedObjects; }; -/** - * Converts a path to an array of its fragments. - * Supports strings, numbers and arrays: - * - * 'foo' => [ 'foo' ] - * 2 => [ '2' ] - * [ 'foo', 'bar' ] => [ 'foo', 'bar' ] - * - * @param {string|number|Array} path Path - * @return {Array} Normalized path. - */ -function normalizePath( path ) { - if ( Array.isArray( path ) ) { - return path; - } else if ( typeof path === 'number' ) { - return [ path.toString() ]; - } - - return [ path ]; -} - -/** - * Clones an object. - * Non-object values are returned unchanged. - * - * @param {*} object Object to clone. - * @return {*} Cloned object, or original literal non-object value. - */ -function cloneObject( object ) { - if ( typeof object === 'object' ) { - return { - ...Object.fromEntries( - Object.entries( object ).map( ( [ key, value ] ) => [ - key, - cloneObject( value ), - ] ) - ), - }; - } - - return object; -} - -/** - * Perform an immutable set. - * Handles nullish initial values. - * Clones all nested objects in the specified object. - * - * @param {Object} object Object to set a value in. - * @param {number|string|Array} path Path in the object to modify. - * @param {*} value New value to set. - * @return {Object} Cloned object with the new value set. - */ -export function immutableSet( object, path, value ) { - const normalizedPath = normalizePath( path ); - const newObject = object ? cloneObject( object ) : {}; - - normalizedPath.reduce( ( acc, key, i ) => { - if ( acc[ key ] === undefined ) { - acc[ key ] = {}; - } - if ( i === normalizedPath.length - 1 ) { - acc[ key ] = value; - } - return acc[ key ]; - }, newObject ); - - return newObject; -} - export function transformStyles( activeSupports, migrationPaths, @@ -222,6 +153,14 @@ export function useBlockSettings( name, parentLayout ) { const themeColors = useSetting( 'color.palette.theme' ); const defaultColors = useSetting( 'color.palette.default' ); const defaultPalette = useSetting( 'color.defaultPalette' ); + const userGradientPalette = useSetting( 'color.gradients.custom' ); + const themeGradientPalette = useSetting( 'color.gradients.theme' ); + const defaultGradientPalette = useSetting( 'color.gradients.default' ); + const defaultGradients = useSetting( 'color.defaultGradients' ); + const areCustomGradientsEnabled = useSetting( 'color.customGradient' ); + const isBackgroundEnabled = useSetting( 'color.background' ); + const isLinkEnabled = useSetting( 'color.link' ); + const isTextEnabled = useSetting( 'color.text' ); const rawSettings = useMemo( () => { return { @@ -231,8 +170,18 @@ export function useBlockSettings( name, parentLayout ) { theme: themeColors, default: defaultColors, }, + gradients: { + custom: userGradientPalette, + theme: themeGradientPalette, + default: defaultGradientPalette, + }, + defaultGradients, defaultPalette, custom: customColorsEnabled, + customGradient: areCustomGradientsEnabled, + background: isBackgroundEnabled, + link: isLinkEnabled, + text: isTextEnabled, }, typography: { fontFamilies: { @@ -299,6 +248,14 @@ export function useBlockSettings( name, parentLayout ) { themeColors, defaultColors, defaultPalette, + userGradientPalette, + themeGradientPalette, + defaultGradientPalette, + defaultGradients, + areCustomGradientsEnabled, + isBackgroundEnabled, + isLinkEnabled, + isTextEnabled, ] ); return useSettingsForBlockElement( rawSettings, name ); diff --git a/packages/block-editor/src/layouts/grid.js b/packages/block-editor/src/layouts/grid.js new file mode 100644 index 00000000000000..69347123fd4213 --- /dev/null +++ b/packages/block-editor/src/layouts/grid.js @@ -0,0 +1,172 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +import { + BaseControl, + Flex, + FlexItem, + RangeControl, + __experimentalUnitControl as UnitControl, + __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { appendSelectors, getBlockGapCSS } from './utils'; +import { getGapCSSValue } from '../hooks/gap'; +import { shouldSkipSerialization } from '../hooks/utils'; + +const RANGE_CONTROL_MAX_VALUES = { + px: 600, + '%': 100, + vw: 100, + vh: 100, + em: 38, + rem: 38, +}; + +export default { + name: 'grid', + label: __( 'Grid' ), + inspectorControls: function GridLayoutInspectorControls( { + layout = {}, + onChange, + } ) { + return ( + + ); + }, + toolBarControls: function DefaultLayoutToolbarControls() { + return null; + }, + getLayoutStyle: function getLayoutStyle( { + selector, + layout, + style, + blockName, + hasBlockGapSupport, + layoutDefinitions, + } ) { + const { minimumColumnWidth = '12rem' } = layout; + + // If a block's block.json skips serialization for spacing or spacing.blockGap, + // don't apply the user-defined value to the styles. + const blockGapValue = + style?.spacing?.blockGap && + ! shouldSkipSerialization( blockName, 'spacing', 'blockGap' ) + ? getGapCSSValue( style?.spacing?.blockGap, '0.5em' ) + : undefined; + + let output = ''; + const rules = []; + + if ( minimumColumnWidth ) { + rules.push( + `grid-template-columns: repeat(auto-fill, minmax(min(${ minimumColumnWidth }, 100%), 1fr))` + ); + } + + if ( rules.length ) { + // Reason to disable: the extra line breaks added by prettier mess with the unit tests. + // eslint-disable-next-line prettier/prettier + output = `${ appendSelectors( selector ) } { ${ rules.join( + '; ' + ) }; }`; + } + + // Output blockGap styles based on rules contained in layout definitions in theme.json. + if ( hasBlockGapSupport && blockGapValue ) { + output += getBlockGapCSS( + selector, + layoutDefinitions, + 'grid', + blockGapValue + ); + } + return output; + }, + getOrientation() { + return 'horizontal'; + }, + getAlignments() { + return []; + }, +}; + +// Enables setting minimum width of grid items. +function GridLayoutMinimumWidthControl( { layout, onChange } ) { + const { minimumColumnWidth: value = '12rem' } = layout; + const [ quantity, unit ] = parseQuantityAndUnitFromRawValue( value ); + + const handleSliderChange = ( next ) => { + onChange( { + ...layout, + minimumColumnWidth: [ next, unit ].join( '' ), + } ); + }; + + // Mostly copied from HeightControl. + const handleUnitChange = ( newUnit ) => { + // Attempt to smooth over differences between currentUnit and newUnit. + // This should slightly improve the experience of switching between unit types. + let newValue; + + if ( [ 'em', 'rem' ].includes( newUnit ) && unit === 'px' ) { + // Convert pixel value to an approximate of the new unit, assuming a root size of 16px. + newValue = ( quantity / 16 ).toFixed( 2 ) + newUnit; + } else if ( [ 'em', 'rem' ].includes( unit ) && newUnit === 'px' ) { + // Convert to pixel value assuming a root size of 16px. + newValue = Math.round( quantity * 16 ) + newUnit; + } else if ( + [ 'vh', 'vw', '%' ].includes( newUnit ) && + quantity > 100 + ) { + // When converting to `vh`, `vw`, or `%` units, cap the new value at 100. + newValue = 100 + newUnit; + } + + onChange( { + ...layout, + minimumColumnWidth: newValue, + } ); + }; + + return ( +
+ + { __( 'Minimum column width' ) } + + + + { + onChange( { + ...layout, + minimumColumnWidth: newValue, + } ); + } } + onUnitChange={ handleUnitChange } + value={ value } + min={ 0 } + /> + + + + + +
+ ); +} diff --git a/packages/block-editor/src/layouts/index.js b/packages/block-editor/src/layouts/index.js index 4ec1ff6f331911..1d30ac3ad36833 100644 --- a/packages/block-editor/src/layouts/index.js +++ b/packages/block-editor/src/layouts/index.js @@ -4,8 +4,9 @@ import flex from './flex'; import flow from './flow'; import constrained from './constrained'; +import grid from './grid'; -const layoutTypes = [ flow, flex, constrained ]; +const layoutTypes = [ flow, flex, constrained, grid ]; /** * Retrieves a layout type by name. diff --git a/packages/block-editor/src/layouts/test/grid.js b/packages/block-editor/src/layouts/test/grid.js new file mode 100644 index 00000000000000..634457670f0dfa --- /dev/null +++ b/packages/block-editor/src/layouts/test/grid.js @@ -0,0 +1,21 @@ +/** + * Internal dependencies + */ +import grid from '../grid'; + +describe( 'getLayoutStyle', () => { + it( 'should return a single `grid-template-columns` property if no non-default params are provided', () => { + const expected = `.editor-styles-wrapper .my-container { grid-template-columns: repeat(auto-fill, minmax(min(12rem, 100%), 1fr)); }`; + + const result = grid.getLayoutStyle( { + selector: '.my-container', + layout: {}, + style: {}, + blockName: 'test-block', + hasBlockGapSupport: false, + layoutDefinitions: undefined, + } ); + + expect( result ).toBe( expected ); + } ); +} ); diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index b34f35adc5c911..567849f66f019e 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -7,6 +7,7 @@ import { lock } from './lock-unlock'; import OffCanvasEditor from './components/off-canvas-editor'; import LeafMoreMenu from './components/off-canvas-editor/leaf-more-menu'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; +import { PrivateListView } from './components/list-view'; /** * Private @wordpress/block-editor APIs. @@ -18,4 +19,5 @@ lock( privateApis, { LeafMoreMenu, OffCanvasEditor, PrivateInserter, + PrivateListView, } ); diff --git a/packages/block-editor/src/utils/object.js b/packages/block-editor/src/utils/object.js new file mode 100644 index 00000000000000..5c6afbc7ba4ff2 --- /dev/null +++ b/packages/block-editor/src/utils/object.js @@ -0,0 +1,69 @@ +/** + * Converts a path to an array of its fragments. + * Supports strings, numbers and arrays: + * + * 'foo' => [ 'foo' ] + * 2 => [ '2' ] + * [ 'foo', 'bar' ] => [ 'foo', 'bar' ] + * + * @param {string|number|Array} path Path + * @return {Array} Normalized path. + */ +function normalizePath( path ) { + if ( Array.isArray( path ) ) { + return path; + } else if ( typeof path === 'number' ) { + return [ path.toString() ]; + } + + return [ path ]; +} + +/** + * Clones an object. + * Non-object values are returned unchanged. + * + * @param {*} object Object to clone. + * @return {*} Cloned object, or original literal non-object value. + */ +function cloneObject( object ) { + if ( typeof object === 'object' ) { + return { + ...Object.fromEntries( + Object.entries( object ).map( ( [ key, value ] ) => [ + key, + cloneObject( value ), + ] ) + ), + }; + } + + return object; +} + +/** + * Perform an immutable set. + * Handles nullish initial values. + * Clones all nested objects in the specified object. + * + * @param {Object} object Object to set a value in. + * @param {number|string|Array} path Path in the object to modify. + * @param {*} value New value to set. + * @return {Object} Cloned object with the new value set. + */ +export function immutableSet( object, path, value ) { + const normalizedPath = normalizePath( path ); + const newObject = object ? cloneObject( object ) : {}; + + normalizedPath.reduce( ( acc, key, i ) => { + if ( acc[ key ] === undefined ) { + acc[ key ] = {}; + } + if ( i === normalizedPath.length - 1 ) { + acc[ key ] = value; + } + return acc[ key ]; + }, newObject ); + + return newObject; +} diff --git a/packages/block-editor/src/utils/test/object.js b/packages/block-editor/src/utils/test/object.js new file mode 100644 index 00000000000000..be5360064ac08b --- /dev/null +++ b/packages/block-editor/src/utils/test/object.js @@ -0,0 +1,107 @@ +/** + * Internal dependencies + */ +import { immutableSet } from '../object'; + +describe( 'immutableSet', () => { + describe( 'handling falsy values properly', () => { + it( 'should create a new object if `undefined` is passed', () => { + const result = immutableSet( undefined, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if `null` is passed', () => { + const result = immutableSet( null, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if `false` is passed', () => { + const result = immutableSet( false, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if `0` is passed', () => { + const result = immutableSet( 0, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if an empty string is passed', () => { + const result = immutableSet( '', 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'should create a new object if a NaN is passed', () => { + const result = immutableSet( NaN, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + } ); + + describe( 'manages data assignment properly', () => { + it( 'assigns value properly when it does not exist', () => { + const result = immutableSet( {}, 'test', 1 ); + + expect( result ).toEqual( { test: 1 } ); + } ); + + it( 'overrides existing values', () => { + const result = immutableSet( { test: 1 }, 'test', 2 ); + + expect( result ).toEqual( { test: 2 } ); + } ); + + describe( 'with array notation access', () => { + it( 'assigns values at deeper levels', () => { + const result = immutableSet( {}, [ 'foo', 'bar', 'baz' ], 5 ); + + expect( result ).toEqual( { foo: { bar: { baz: 5 } } } ); + } ); + + it( 'overrides existing values at deeper levels', () => { + const result = immutableSet( + { foo: { bar: { baz: 1 } } }, + [ 'foo', 'bar', 'baz' ], + 5 + ); + + expect( result ).toEqual( { foo: { bar: { baz: 5 } } } ); + } ); + + it( 'keeps other properties intact', () => { + const result = immutableSet( + { foo: { bar: { baz: 1 } } }, + [ 'foo', 'bar', 'test' ], + 5 + ); + + expect( result ).toEqual( { + foo: { bar: { baz: 1, test: 5 } }, + } ); + } ); + } ); + } ); + + describe( 'does not mutate the original object', () => { + it( 'clones the object at the first level', () => { + const input = {}; + const result = immutableSet( input, 'test', 1 ); + + expect( result ).not.toBe( input ); + } ); + + it( 'clones the object at deeper levels', () => { + const input = { foo: { bar: { baz: 1 } } }; + const result = immutableSet( input, [ 'foo', 'bar', 'baz' ], 2 ); + + expect( result ).not.toBe( input ); + expect( result.foo ).not.toBe( input.foo ); + expect( result.foo.bar ).not.toBe( input.foo.bar ); + expect( result.foo.bar.baz ).not.toBe( input.foo.bar.baz ); + } ); + } ); +} ); diff --git a/packages/block-library/src/buttons/block.json b/packages/block-library/src/buttons/block.json index 266646d96bcee5..71d5aa302cfc9b 100644 --- a/packages/block-library/src/buttons/block.json +++ b/packages/block-library/src/buttons/block.json @@ -10,6 +10,7 @@ "supports": { "anchor": true, "align": [ "wide", "full" ], + "html": false, "__experimentalExposeControlsToChildren": true, "spacing": { "blockGap": true, diff --git a/packages/block-library/src/categories/editor.scss b/packages/block-library/src/categories/editor.scss index cf112b6fc5ff63..773a65296c06ec 100644 --- a/packages/block-library/src/categories/editor.scss +++ b/packages/block-library/src/categories/editor.scss @@ -5,3 +5,8 @@ margin-top: 6px; } } + +/* Center alignment for classic themes. */ +[data-align="center"] .wp-block-categories { + text-align: center; +} diff --git a/packages/block-library/src/categories/style.scss b/packages/block-library/src/categories/style.scss index 8cf1252800b438..b87e28a80f0f36 100644 --- a/packages/block-library/src/categories/style.scss +++ b/packages/block-library/src/categories/style.scss @@ -10,4 +10,8 @@ /*rtl:ignore*/ margin-left: 2em; } + /* Only apply the text align on dropdowns, not lists. */ + &.wp-block-categories-dropdown.aligncenter { + text-align: center; + } } diff --git a/packages/block-library/src/columns/block.json b/packages/block-library/src/columns/block.json index aaaa72263e309f..df7245939986db 100644 --- a/packages/block-library/src/columns/block.json +++ b/packages/block-library/src/columns/block.json @@ -13,6 +13,10 @@ "isStackedOnMobile": { "type": "boolean", "default": true + }, + "templateLock": { + "type": [ "string", "boolean" ], + "enum": [ "all", "insert", "contentOnly", false ] } }, "supports": { diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js index e995729099961b..270ddded6cfabd 100644 --- a/packages/block-library/src/columns/edit.js +++ b/packages/block-library/src/columns/edit.js @@ -58,7 +58,7 @@ function ColumnsEditContainer( { updateColumns, clientId, } ) { - const { isStackedOnMobile, verticalAlignment } = attributes; + const { isStackedOnMobile, verticalAlignment, templateLock } = attributes; const { count, canInsertColumnBlock } = useSelect( ( select ) => { @@ -84,6 +84,7 @@ function ColumnsEditContainer( { allowedBlocks: ALLOWED_BLOCKS, orientation: 'horizontal', renderAppender: false, + templateLock, } ); return ( diff --git a/packages/block-library/src/comments/index.php b/packages/block-library/src/comments/index.php index a5e51cfbf4e793..807c0c3ead2b8f 100644 --- a/packages/block-library/src/comments/index.php +++ b/packages/block-library/src/comments/index.php @@ -212,6 +212,7 @@ function register_legacy_post_comments_block() { * like `_wp_multiple_block_styles`, which is required in this case because * the block has multiple styles. */ + /** This filter is documented in wp-includes/blocks.php */ $metadata = apply_filters( 'block_type_metadata', $metadata ); register_block_type( 'core/post-comments', $metadata ); diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index fcacf44e1c6851..f99180d7601fdd 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -206,9 +206,7 @@ export default function CoverInspectorControls( { isImgElement && ( setAttributes( { alt: newAlt } ) @@ -217,11 +215,12 @@ export default function CoverInspectorControls( { <> { __( - 'Describe the purpose of the image' + 'Describe the purpose of the image.' ) } +
{ __( - 'Leave empty if the image is purely decorative.' + 'Leave empty if decorative.' ) } } diff --git a/packages/block-library/src/cover/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/cover/test/__snapshots__/transforms.native.js.snap index 252edf24172231..dc4ba0fbb2b1f8 100644 --- a/packages/block-library/src/cover/test/__snapshots__/transforms.native.js.snap +++ b/packages/block-library/src/cover/test/__snapshots__/transforms.native.js.snap @@ -30,7 +30,7 @@ exports[`Cover block transformations with Image to Image block 1`] = ` exports[`Cover block transformations with Image to Media & Text block 1`] = ` " -
+

Cool cover

" @@ -60,7 +60,7 @@ exports[`Cover block transformations with Video to Group block 1`] = ` exports[`Cover block transformations with Video to Media & Text block 1`] = ` " -
+

Cool cover

" diff --git a/packages/block-library/src/cover/test/block-controls.js b/packages/block-library/src/cover/test/block-controls.js deleted file mode 100644 index ea14dfb38d7b73..00000000000000 --- a/packages/block-library/src/cover/test/block-controls.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * External dependencies - */ -import { render, screen, fireEvent } from '@testing-library/react'; - -// Need to mock the BlockControls wrapper as this requires a slot to run -// so can't be easily unit tested. -jest.mock( '@wordpress/block-editor', () => ( { - ...jest.requireActual( '@wordpress/block-editor' ), - BlockControls: ( { children } ) =>
{ children }
, -} ) ); - -/** - * Internal dependencies - */ -import CoverBlockControls from '../edit/block-controls'; - -const setAttributes = jest.fn(); -const onSelectMedia = jest.fn(); - -const currentSettings = { hasInnerBlocks: true, url: undefined }; -const defaultAttributes = { - contentPosition: undefined, - id: 1, - useFeaturedImage: false, - dimRatio: 50, - minHeight: 300, - minHeightUnit: 'px', -}; -const defaultProps = { - attributes: defaultAttributes, - currentSettings, - setAttributes, - onSelectMedia, -}; - -beforeEach( () => { - setAttributes.mockClear(); - onSelectMedia.mockClear(); -} ); - -describe( 'Cover block controls', () => { - describe( 'Full height toggle', () => { - test( 'displays toggle full height button toggled off if minHeight not 100vh', () => { - render( ); - expect( - screen.getByRole( 'button', { - pressed: false, - name: 'Toggle full height', - } ) - ).toBeInTheDocument(); - } ); - test( 'sets minHeight attributes to 100vh when clicked', () => { - render( ); - fireEvent.click( screen.getByLabelText( 'Toggle full height' ) ); - expect( setAttributes ).toHaveBeenCalledWith( { - minHeight: 100, - minHeightUnit: 'vh', - } ); - } ); - } ); -} ); diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js new file mode 100644 index 00000000000000..c2510dcb666b6c --- /dev/null +++ b/packages/block-library/src/cover/test/edit.js @@ -0,0 +1,324 @@ +/** + * External dependencies + */ +import { screen, fireEvent, act, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import { + initializeEditor, + selectBlock, +} from 'test/integration/helpers/integration-test-editor'; + +async function setup( attributes ) { + const testBlock = { name: 'core/cover', attributes }; + return initializeEditor( testBlock ); +} + +async function createAndSelectBlock() { + await userEvent.click( + screen.getByRole( 'button', { + name: 'Color: Black', + } ) + ); + await userEvent.click( + screen.getByRole( 'button', { + name: 'Select Cover', + } ) + ); +} + +describe( 'Cover block', () => { + describe( 'Editor canvas', () => { + test( 'shows placeholder if background image and color not set', async () => { + await setup(); + + expect( + screen.getByRole( 'group', { + name: 'To edit this block, you need permission to upload media.', + } ) + ).toBeInTheDocument(); + } ); + + test( 'can set overlay color using color picker on block placeholder', async () => { + const { container } = await setup(); + const colorPicker = screen.getByRole( 'button', { + name: 'Color: Black', + } ); + await userEvent.click( colorPicker ); + const color = colorPicker.style.backgroundColor; + expect( + screen.queryByRole( 'group', { + name: 'To edit this block, you need permission to upload media.', + } ) + ).not.toBeInTheDocument(); + + // eslint-disable-next-line testing-library/no-node-access + const overlay = container.getElementsByClassName( + 'wp-block-cover__background' + ); + expect( overlay[ 0 ] ).toHaveStyle( + `background-color: ${ color }` + ); + } ); + + test( 'can have the title edited', async () => { + await setup(); + + await userEvent.click( + screen.getByRole( 'button', { + name: 'Color: Black', + } ) + ); + + const title = screen.getByLabelText( 'Empty block;', { + exact: false, + } ); + await userEvent.click( title ); + await userEvent.keyboard( 'abc' ); + expect( title ).toHaveTextContent( 'abc' ); + } ); + } ); + + describe( 'Block toolbar', () => { + test( 'full height toggle sets minHeight style attribute to 100vh when clicked', async () => { + await setup(); + await createAndSelectBlock(); + + expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveStyle( + 'min-height: 100vh;' + ); + + await userEvent.click( + screen.getByLabelText( 'Toggle full height' ) + ); + + expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveStyle( + 'min-height: 100vh;' + ); + } ); + + test( 'content position button sets content position', async () => { + await setup(); + await createAndSelectBlock(); + + await userEvent.click( + screen.getByLabelText( 'Change content position' ) + ); + + expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveClass( + 'has-custom-content-position' + ); + + await act( async () => + within( screen.getByRole( 'grid' ) ) + .getByRole( 'gridcell', { + name: 'top left', + } ) + .focus() + ); + + expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass( + 'has-custom-content-position' + ); + expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass( + 'is-position-top-left' + ); + } ); + } ); + + describe( 'Inspector controls', () => { + describe( 'Media settings', () => { + test( 'does not display media settings panel if url is not set', async () => { + await setup(); + expect( + screen.queryByRole( 'button', { + name: 'Media settings', + } ) + ).not.toBeInTheDocument(); + } ); + test( 'displays media settings panel if url is set', async () => { + await setup( { + url: 'http://localhost/my-image.jpg', + } ); + + await selectBlock( 'Block: Cover' ); + expect( + screen.getByRole( 'button', { + name: 'Media settings', + } ) + ).toBeInTheDocument(); + } ); + } ); + + test( 'sets hasParallax attribute to true if fixed background toggled', async () => { + await setup( { + url: 'http://localhost/my-image.jpg', + } ); + expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveClass( + 'has-parallax' + ); + await selectBlock( 'Block: Cover' ); + await userEvent.click( + screen.getByLabelText( 'Fixed background' ) + ); + expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass( + 'has-parallax' + ); + } ); + + test( 'sets isRepeated attribute to true if repeated background toggled', async () => { + await setup( { + url: 'http://localhost/my-image.jpg', + } ); + expect( screen.getByLabelText( 'Block: Cover' ) ).not.toHaveClass( + 'is-repeated' + ); + await selectBlock( 'Block: Cover' ); + await userEvent.click( + screen.getByLabelText( 'Repeated background' ) + ); + expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass( + 'is-repeated' + ); + } ); + + test( 'sets left focalPoint attribute when focal point values changed', async () => { + await setup( { + url: 'http://localhost/my-image.jpg', + } ); + + await selectBlock( 'Block: Cover' ); + await userEvent.clear( screen.getByLabelText( 'Left' ) ); + await userEvent.type( screen.getByLabelText( 'Left' ), '100' ); + + expect( + within( screen.getByLabelText( 'Block: Cover' ) ).getByRole( + 'img' + ) + ).toHaveStyle( 'object-position: 100% 50%;' ); + } ); + + test( 'sets alt attribute if text entered in alt text box', async () => { + await setup( { + url: 'http://localhost/my-image.jpg', + } ); + + await selectBlock( 'Block: Cover' ); + await userEvent.type( + screen.getByLabelText( 'Alt text (alternative text)' ), + 'Me' + ); + expect( screen.getByAltText( 'Me' ) ).toBeInTheDocument(); + } ); + + test( 'clears media when clear media button clicked', async () => { + await setup( { + url: 'http://localhost/my-image.jpg', + } ); + + await selectBlock( 'Block: Cover' ); + expect( + within( screen.getByLabelText( 'Block: Cover' ) ).getByRole( + 'img' + ) + ).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole( 'button', { + name: 'Clear Media', + } ) + ); + expect( + within( screen.queryByLabelText( 'Block: Cover' ) ).queryByRole( + 'img' + ) + ).not.toBeInTheDocument(); + } ); + + describe( 'Color panel', () => { + test( 'applies selected opacity to block when number control value changed', async () => { + const { container } = await setup(); + + await createAndSelectBlock(); + + // eslint-disable-next-line testing-library/no-node-access + const overlay = container.getElementsByClassName( + 'wp-block-cover__background' + ); + + expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-100' ); + + await userEvent.click( + screen.getByRole( 'tab', { + name: 'Styles', + } ) + ); + + fireEvent.change( + screen.getByRole( 'spinbutton', { + name: 'Overlay opacity', + } ), + { + target: { value: '40' }, + } + ); + + expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-40' ); + } ); + + test( 'applies selected opacity to block when slider moved', async () => { + const { container } = await setup(); + + await createAndSelectBlock(); + + // eslint-disable-next-line testing-library/no-node-access + const overlay = container.getElementsByClassName( + 'wp-block-cover__background' + ); + + expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-100' ); + + await userEvent.click( + screen.getByRole( 'tab', { + name: 'Styles', + } ) + ); + + fireEvent.change( + screen.getByRole( 'slider', { + name: 'Overlay opacity', + } ), + { target: { value: 30 } } + ); + + expect( overlay[ 0 ] ).toHaveClass( 'has-background-dim-30' ); + } ); + } ); + + describe( 'Dimensions panel', () => { + test( 'sets minHeight attribute when number control value changed', async () => { + await setup(); + await createAndSelectBlock(); + await userEvent.click( + screen.getByRole( 'tab', { + name: 'Styles', + } ) + ); + await userEvent.clear( + screen.getByLabelText( 'Minimum height of cover' ) + ); + await userEvent.type( + screen.getByLabelText( 'Minimum height of cover' ), + '300' + ); + + expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveStyle( + 'min-height: 300px;' + ); + } ); + } ); + } ); +} ); diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 7ab1b1307056f5..64a6dae10ec90f 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -562,6 +562,7 @@ function GalleryEdit( props ) { max={ Math.min( MAX_COLUMNS, images.length ) } { ...MOBILE_CONTROL_PROPS_RANGE_CONTROL } required + size="__unstable-large" /> ) } { hasLinkTo && ( 0 && ( ) } { Platform.isWeb && ! imageSizeOptions && hasImageIds && ( - { __( 'Image size' ) } + { __( 'Resolution' ) } diff --git a/packages/block-library/src/gallery/use-image-sizes.js b/packages/block-library/src/gallery/use-image-sizes.js index 133b421cfa12e9..c96cd8dbaf5781 100644 --- a/packages/block-library/src/gallery/use-image-sizes.js +++ b/packages/block-library/src/gallery/use-image-sizes.js @@ -5,7 +5,7 @@ import { useMemo } from '@wordpress/element'; /** * Calculates the image sizes that are avaible for the current gallery images in order to - * populate the 'Image size' selector. + * populate the 'Resolution' selector. * * @param {Array} images Basic image block data taken from current gallery innerBlock * @param {boolean} isSelected Is the block currently selected in the editor. diff --git a/packages/block-library/src/group/deprecated.js b/packages/block-library/src/group/deprecated.js index dc523d9d512a32..af52b14c7c5e6e 100644 --- a/packages/block-library/src/group/deprecated.js +++ b/packages/block-library/src/group/deprecated.js @@ -50,7 +50,8 @@ const deprecated = [ default: 'div', }, templateLock: { - type: 'string', + type: [ 'string', 'boolean' ], + enum: [ 'all', 'insert', false ], }, }, supports: { @@ -135,7 +136,8 @@ const deprecated = [ default: 'div', }, templateLock: { - type: 'string', + type: [ 'string', 'boolean' ], + enum: [ 'all', 'insert', false ], }, }, supports: { diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index 452da6e7f32386..04c9e71ca853b8 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -12,6 +12,7 @@ import { } from '@wordpress/block-editor'; import { SelectControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { View } from '@wordpress/primitives'; /** * Internal dependencies @@ -97,7 +98,8 @@ function GroupEdit( { ? { ...defaultLayout, ...layout, type: 'default' } : { ...defaultLayout, ...layout }; const { type = 'default' } = usedLayout; - const layoutSupportEnabled = themeSupportsLayout || type === 'flex'; + const layoutSupportEnabled = + themeSupportsLayout || type === 'flex' || type === 'grid'; // Hooks. const blockProps = useBlockProps( { @@ -108,15 +110,28 @@ function GroupEdit( { usedLayoutType: usedLayout?.type, hasInnerBlocks, } ); + + // Default to the regular appender being rendered. + let renderAppender; + if ( showPlaceholder ) { + // In the placeholder state, ensure the appender is not rendered. + // This is needed because `...innerBlocksProps` is used in the placeholder + // state so that blocks can dragged onto the placeholder area + // from both the list view and in the editor canvas. + renderAppender = false; + } else if ( ! hasInnerBlocks ) { + // When there is no placeholder, but the block is also empty, + // use the larger button appender. + renderAppender = InnerBlocks.ButtonBlockAppender; + } + const innerBlocksProps = useInnerBlocksProps( layoutSupportEnabled ? blockProps : { className: 'wp-block-group__inner-container' }, { templateLock, - renderAppender: hasInnerBlocks - ? undefined - : InnerBlocks.ButtonBlockAppender, + renderAppender, __unstableDisableLayoutClassNames: ! layoutSupportEnabled, } ); @@ -138,11 +153,14 @@ function GroupEdit( { } /> { showPlaceholder && ( - + + { innerBlocksProps.children } + + ) } { layoutSupportEnabled && ! showPlaceholder && ( diff --git a/packages/block-library/src/group/placeholder.js b/packages/block-library/src/group/placeholder.js index daf535df8bf607..16a99a9287338d 100644 --- a/packages/block-library/src/group/placeholder.js +++ b/packages/block-library/src/group/placeholder.js @@ -47,6 +47,17 @@ const getGroupPlaceholderIcons = ( name = 'group' ) => { ), + 'group-grid': ( + + + + + ), }; return icons?.[ name ]; }; @@ -85,7 +96,8 @@ export function useShouldShowPlaceHolder( { ! fontSize && ! textColor && ! style && - usedLayoutType !== 'flex' + usedLayoutType !== 'flex' && + usedLayoutType !== 'grid' ); useEffect( () => { diff --git a/packages/block-library/src/group/variations.js b/packages/block-library/src/group/variations.js index 2b64d794dd4cc2..3f5dcc0a45a9e4 100644 --- a/packages/block-library/src/group/variations.js +++ b/packages/block-library/src/group/variations.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __, _x } from '@wordpress/i18n'; -import { group, row, stack } from '@wordpress/icons'; +import { group, row, stack, grid } from '@wordpress/icons'; const variations = [ { @@ -44,4 +44,17 @@ const variations = [ }, ]; +if ( window?.__experimentalEnableGroupGridVariation ) { + variations.push( { + name: 'group-grid', + title: __( 'Grid' ), + description: __( 'Arrange blocks in a grid.' ), + attributes: { layout: { type: 'grid' } }, + scope: [ 'block', 'inserter', 'transform' ], + isActive: ( blockAttributes ) => + blockAttributes.layout?.type === 'grid', + icon: grid, + } ); +} + export default variations; diff --git a/packages/block-library/src/home-link/index.php b/packages/block-library/src/home-link/index.php index 33f41057c936bb..459979b0d43893 100644 --- a/packages/block-library/src/home-link/index.php +++ b/packages/block-library/src/home-link/index.php @@ -98,12 +98,12 @@ function block_core_home_link_build_li_wrapper_attributes( $context ) { $colors['css_classes'], $font_sizes['css_classes'] ); + $classes[] = 'wp-block-navigation-item'; $style_attribute = ( $colors['inline_styles'] . $font_sizes['inline_styles'] ); - $css_classes = trim( implode( ' ', $classes ) ) . ' wp-block-navigation-item'; $wrapper_attributes = get_block_wrapper_attributes( array( - 'class' => $css_classes, + 'class' => implode( ' ', $classes ), 'style' => $style_attribute, ) ); diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index 62edb882be0c93..b54358fc48d6a4 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -85,7 +85,7 @@ "supports": { "anchor": true, "color": { - "__experimentalDuotone": "img, .components-placeholder", + "__experimentalDuotone": true, "text": false, "background": false }, @@ -93,7 +93,6 @@ "color": true, "radius": true, "width": true, - "__experimentalSelector": "img, .wp-block-image__crop-area", "__experimentalSkipSerialization": true, "__experimentalDefaultControls": { "color": true, @@ -102,6 +101,12 @@ } } }, + "selectors": { + "border": ".wp-block-image img, .wp-block-image .wp-block-image__crop-area", + "filter": { + "duotone": "img, .components-placeholder" + } + }, "styles": [ { "name": "default", diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index d69fbd1c9638df..175c0b8bcf21a7 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -211,7 +211,7 @@ export function ImageEdit( { : 'full', }; } else { - // Keep the same url when selecting the same file, so "Image Size" + // Keep the same url when selecting the same file, so "Resolution" // option is not changed. additionalAttributes = { url }; } diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 3ffd377005af92..804ae9e1671f6e 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -578,7 +578,7 @@ export class ImageEdit extends Component { footerNote={ <> { __( - 'Describe the purpose of the image. Leave empty if the image is purely decorative.' + 'Describe the purpose of the image. Leave empty if decorative.' ) }{ ' ' } image?.media_details?.sizes?.[ slug ]?.source_url ) .map( ( { name, slug } ) => ( { value: slug, label: name } ) ); + const canUploadMedia = !! mediaUpload; // If an image is externally hosted, try to fetch the image data. This may // fail if the image host doesn't allow CORS with the domain. If it works, // we can enable a button in the toolbar to upload the image. useEffect( () => { - if ( ! isExternalImage( id, url ) || ! isSelected || externalBlob ) { + if ( + ! isExternalImage( id, url ) || + ! isSelected || + ! canUploadMedia || + externalBlob + ) { return; } @@ -185,7 +191,7 @@ export default function Image( { .then( ( blob ) => setExternalBlob( blob ) ) // Do nothing, cannot upload. .catch( () => {} ); - }, [ id, url, isSelected, externalBlob ] ); + }, [ id, url, isSelected, externalBlob, canUploadMedia ] ); // We need to show the caption when changes come from // history navigation(undo/redo). @@ -401,19 +407,18 @@ export default function Image( { { ! multiImageSelection && ( { __( - 'Describe the purpose of the image' + 'Describe the purpose of the image.' ) } - { __( - 'Leave empty if the image is purely decorative.' - ) } +
+ { __( 'Leave empty if decorative.' ) } } /> @@ -428,6 +433,9 @@ export default function Image( { isResizable={ isResizable } imageWidth={ naturalWidth } imageHeight={ naturalHeight } + imageSizeHelp={ __( + 'Select the size of the source image.' + ) } /> diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 4f373558ed4373..e05939a4d0feac 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -14,13 +14,18 @@ * @return string Returns the block content with the data-id attribute added. */ function render_block_core_image( $attributes, $content ) { + $processor = new WP_HTML_Tag_Processor( $content ); + $processor->next_tag( 'img' ); + + if ( $processor->get_attribute( 'src' ) === null ) { + return ''; + } + if ( isset( $attributes['data-id'] ) ) { // Add the data-id="$id" attribute to the img element // to provide backwards compatibility for the Gallery Block, // which now wraps Image Blocks within innerBlocks. // The data-id attribute is added in a core/gallery `render_block_data` hook. - $processor = new WP_HTML_Tag_Processor( $content ); - $processor->next_tag( 'img' ); $processor->set_attribute( 'data-id', $attributes['data-id'] ); $content = $processor->get_updated_html(); } diff --git a/packages/block-library/src/image/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/image/test/__snapshots__/transforms.native.js.snap index 61c8de02bd03a2..56a5fe3c259120 100644 --- a/packages/block-library/src/image/test/__snapshots__/transforms.native.js.snap +++ b/packages/block-library/src/image/test/__snapshots__/transforms.native.js.snap @@ -42,7 +42,7 @@ exports[`Image block transformations to Group block 1`] = ` exports[`Image block transformations to Media & Text block 1`] = ` " -
+

" diff --git a/packages/block-library/src/latest-comments/style.scss b/packages/block-library/src/latest-comments/style.scss index 604729f763caa0..8759da8c73d94d 100644 --- a/packages/block-library/src/latest-comments/style.scss +++ b/packages/block-library/src/latest-comments/style.scss @@ -1,7 +1,7 @@ // Lower specificity - target list element. ol.wp-block-latest-comments { // Removes left spacing in Customizer Widgets screen. - // Due to low specificity this will be safely overriden + // Due to low specificity this will be safely overridden // by default wp-block layout styles in the Post/Site editor margin-left: 0; diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index 2e10cb19de0685..45266f6ab94c4e 100644 --- a/packages/block-library/src/latest-posts/edit.js +++ b/packages/block-library/src/latest-posts/edit.js @@ -289,6 +289,9 @@ export default function LatestPostsEdit( { attributes, setAttributes } ) { imageWidth={ defaultImageWidth } imageHeight={ defaultImageHeight } imageSizeOptions={ imageSizeOptions } + imageSizeHelp={ __( + 'Select the size of the source image.' + ) } onChangeImage={ ( value ) => setAttributes( { featuredImageSizeSlug: value, diff --git a/packages/block-library/src/media-text/block.json b/packages/block-library/src/media-text/block.json index 1c9fc0b6820f1e..ded230a2eb1006 100644 --- a/packages/block-library/src/media-text/block.json +++ b/packages/block-library/src/media-text/block.json @@ -10,7 +10,7 @@ "attributes": { "align": { "type": "string", - "default": "wide" + "default": "none" }, "mediaAlt": { "type": "string", diff --git a/packages/block-library/src/media-text/deprecated.js b/packages/block-library/src/media-text/deprecated.js index 95595f8da6424b..2d061213dface8 100644 --- a/packages/block-library/src/media-text/deprecated.js +++ b/packages/block-library/src/media-text/deprecated.js @@ -13,6 +13,7 @@ import { useInnerBlocksProps, useBlockProps, } from '@wordpress/block-editor'; +import { compose } from '@wordpress/compose'; /** * Internal dependencies @@ -30,6 +31,19 @@ const v1ToV5ImageFillStyles = ( url, focalPoint ) => { : {}; }; +const v6ImageFillStyles = ( url, focalPoint ) => { + return url + ? { + backgroundImage: `url(${ url })`, + backgroundPosition: focalPoint + ? `${ Math.round( focalPoint.x * 100 ) }% ${ Math.round( + focalPoint.y * 100 + ) }%` + : `50% 50%`, + } + : {}; +}; + const DEFAULT_MEDIA_WIDTH = 50; const noop = () => {}; @@ -49,6 +63,20 @@ const migrateCustomColors = ( attributes ) => { }; }; +// After align attribute's default was updated this function explicitly sets +// the align value for deprecated blocks to the `wide` value which was default +// for their versions of this block. +const migrateDefaultAlign = ( attributes ) => { + if ( attributes.align ) { + return attributes; + } + + return { + ...attributes, + align: 'wide', + }; +}; + const baseAttributes = { align: { type: 'string', @@ -133,6 +161,40 @@ const v4ToV5BlockAttributes = { }, }; +const v6Attributes = { + ...v4ToV5BlockAttributes, + mediaAlt: { + type: 'string', + source: 'attribute', + selector: 'figure img', + attribute: 'alt', + default: '', + __experimentalRole: 'content', + }, + mediaId: { + type: 'number', + __experimentalRole: 'content', + }, + mediaUrl: { + type: 'string', + source: 'attribute', + selector: 'figure video,figure img', + attribute: 'src', + __experimentalRole: 'content', + }, + href: { + type: 'string', + source: 'attribute', + selector: 'figure a', + attribute: 'href', + __experimentalRole: 'content', + }, + mediaType: { + type: 'string', + __experimentalRole: 'content', + }, +}; + const v4ToV5Supports = { anchor: true, align: [ 'wide', 'full' ], @@ -143,6 +205,166 @@ const v4ToV5Supports = { }, }; +const v6Supports = { + ...v4ToV5Supports, + color: { + gradients: true, + link: true, + __experimentalDefaultControls: { + background: true, + text: true, + }, + }, + spacing: { + margin: true, + padding: true, + }, + typography: { + fontSize: true, + lineHeight: true, + __experimentalFontFamily: true, + __experimentalFontWeight: true, + __experimentalFontStyle: true, + __experimentalTextTransform: true, + __experimentalTextDecoration: true, + __experimentalLetterSpacing: true, + __experimentalDefaultControls: { + fontSize: true, + }, + }, +}; + +// Version with wide as the default alignment. +// See: https://github.com/WordPress/gutenberg/pull/48404 +const v6 = { + attributes: v6Attributes, + supports: v6Supports, + save( { attributes } ) { + const { + isStackedOnMobile, + mediaAlt, + mediaPosition, + mediaType, + mediaUrl, + mediaWidth, + mediaId, + verticalAlignment, + imageFill, + focalPoint, + linkClass, + href, + linkTarget, + rel, + } = attributes; + const mediaSizeSlug = + attributes.mediaSizeSlug || DEFAULT_MEDIA_SIZE_SLUG; + const newRel = isEmpty( rel ) ? undefined : rel; + + const imageClasses = classnames( { + [ `wp-image-${ mediaId }` ]: mediaId && mediaType === 'image', + [ `size-${ mediaSizeSlug }` ]: mediaId && mediaType === 'image', + } ); + + let image = ( + { + ); + + if ( href ) { + image = ( + + { image } + + ); + } + + const mediaTypeRenders = { + image: () => image, + video: () =>