From 8a1d808ac891a4c300d5b5f1f42ee7538ddb5532 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Thu, 16 Nov 2023 19:35:45 +0000 Subject: [PATCH] Merge changes published in the Gutenberg plugin "release/17.1" branch --- .eslintrc.js | 1 + .github/ISSUE_TEMPLATE/Bug_report.yml | 1 + .github/ISSUE_TEMPLATE/Feature_request.md | 1 + .github/ISSUE_TEMPLATE/New_release.md | 1 + .github/workflows/build-plugin-zip.yml | 14 +- .github/workflows/bundle-size.yml | 4 +- .../workflows/check-components-changelog.yml | 2 +- .github/workflows/create-block.yml | 2 +- .github/workflows/end2end-test.yml | 6 +- .github/workflows/enforce-pr-labels.yml | 2 +- .../workflows/gradle-wrapper-validation.yml | 2 +- .github/workflows/performance.yml | 2 +- .github/workflows/php-changes-detection.yml | 4 +- .github/workflows/publish-npm-packages.yml | 10 +- .github/workflows/pull-request-automation.yml | 4 +- .github/workflows/rnmobile-android-runner.yml | 4 +- .github/workflows/rnmobile-ios-runner.yml | 2 +- .github/workflows/static-checks.yml | 4 +- .github/workflows/storybook-pages.yml | 2 +- .github/workflows/unit-test.yml | 12 +- .../upload-release-to-plugin-repo.yml | 2 +- changelog.txt | 462 +++++++++++-- docs/contributors/code/git-workflow.md | 2 +- .../getting-started-react-native.md | 2 +- docs/contributors/code/release.md | 2 +- docs/contributors/documentation/README.md | 4 +- docs/getting-started/create-block/README.md | 18 - .../devenv/get-started-with-create-block.md | 4 +- .../devenv/get-started-with-wp-env.md | 1 + docs/getting-started/quick-start-guide.md | 44 ++ docs/manifest.json | 6 + .../block-api/block-attributes.md | 2 +- .../block-api/block-supports.md | 2 +- docs/reference-guides/core-blocks.md | 2 +- .../data/data-core-block-editor.md | 24 + .../data/data-core-edit-site.md | 21 +- docs/reference-guides/data/data-core.md | 26 + .../reference-guides/filters/block-filters.md | 16 +- docs/toc.json | 1 + gutenberg.php | 2 +- lib/compat/wordpress-6.4/rest-api.php | 9 - ...global-styles-revisions-controller-6-5.php | 102 +++ .../class-wp-navigation-block-renderer.php | 642 ++++++++++++++++++ lib/compat/wordpress-6.5/rest-api.php | 21 + lib/experimental/data-views.php | 2 +- .../fonts-api/class-wp-fonts-resolver.php | 14 +- .../font-library/class-wp-font-family.php | 5 + .../class-wp-rest-font-library-controller.php | 41 +- .../class-wp-directive-processor.php | 99 ++- .../directive-processing.php | 123 +--- lib/load.php | 5 + package-lock.json | 315 +++++++-- package.json | 5 +- packages/base-styles/_z-index.scss | 1 + packages/blob/CHANGELOG.md | 4 + packages/blob/README.md | 25 + packages/blob/src/index.js | 40 ++ packages/blob/src/test/index.js | 59 +- .../block-heading-level-dropdown/index.js | 2 +- .../components/block-list-appender/index.js | 24 +- .../src/components/block-list/index.js | 22 +- .../block-quick-navigation/index.js | 16 +- .../src/components/block-styles/index.js | 10 - .../pattern-transformations-menu.js | 34 +- .../src/components/block-toolbar/style.scss | 8 + .../src/components/block-tools/back-compat.js | 2 +- .../block-tools/block-contextual-toolbar.js | 145 +--- .../block-tools/empty-block-inserter.js | 56 ++ .../src/components/block-tools/index.js | 88 ++- .../block-tools/selected-block-popover.js | 265 -------- .../block-tools/selected-block-tools.js | 127 ++++ .../src/components/block-tools/style.scss | 10 - .../use-selected-block-tool-props.js | 66 ++ .../src/components/editable-text/index.js | 9 +- .../src/components/iframe/index.js | 7 +- .../{explorer.js => index.js} | 8 +- ...sidebar.js => pattern-explorer-sidebar.js} | 0 .../{patterns-list.js => pattern-list.js} | 5 +- .../components/inserter/block-patterns-tab.js | 448 ------------ .../inserter/block-patterns-tab/index.js | 118 ++++ .../pattern-category-preview-panel.js | 48 ++ .../pattern-category-previews.js | 175 +++++ .../patterns-filter.js} | 36 +- .../use-pattern-categories.js | 96 +++ .../inserter/block-patterns-tab/utils.js | 76 +++ .../inserter/media-tab/media-list.js | 14 +- .../inserter/media-tab/media-preview.js | 49 +- .../src/components/inserter/menu.js | 9 +- .../src/components/link-control/README.md | 4 +- .../src/components/link-control/index.js | 21 +- .../src/components/link-control/style.scss | 13 +- .../list-view/block-select-button.js | 45 +- .../src/components/list-view/block.js | 22 +- .../src/components/list-view/index.js | 2 + .../components/media-replace-flow/style.scss | 4 +- .../src/components/navigable-toolbar/index.js | 96 ++- .../src/components/plain-text/README.md | 6 +- .../src/components/provider/use-block-sync.js | 23 +- .../src/components/rich-text/README.md | 17 +- .../src/components/rich-text/index.js | 1 - .../src/components/rich-text/index.native.js | 4 +- .../rich-text/native}/format-edit.js | 5 +- .../native}/get-format-colors.native.js | 4 +- .../src/components/rich-text/native/index.js | 1 + .../rich-text/native}/index.native.js | 24 +- .../rich-text/native}/style.native.scss | 0 .../test/__snapshots__/index.native.js.snap | 0 .../rich-text/native}/test/index.native.js | 6 +- .../test/performance/rich-text.native.js | 2 +- .../toolbar-button-with-options.native.js | 0 .../rich-text/native}/use-format-types.js | 5 +- .../components/rich-text/use-paste-handler.js | 7 +- .../url-popover/image-url-input-ui.js | 1 + .../components/writing-flow/use-tab-nav.js | 11 +- packages/block-editor/src/hooks/align.js | 16 +- .../block-editor/src/hooks/align.native.js | 4 +- packages/block-editor/src/hooks/anchor.js | 44 +- packages/block-editor/src/hooks/background.js | 34 +- .../block-editor/src/hooks/block-hooks.js | 36 +- .../block-editor/src/hooks/block-rename-ui.js | 10 +- .../block-editor/src/hooks/content-lock-ui.js | 6 +- .../src/hooks/custom-class-name.js | 13 +- .../block-editor/src/hooks/custom-fields.js | 10 +- packages/block-editor/src/hooks/duotone.js | 56 +- packages/block-editor/src/hooks/layout.js | 219 +++--- packages/block-editor/src/hooks/position.js | 29 +- packages/block-editor/src/hooks/style.js | 48 +- packages/block-editor/src/hooks/test/align.js | 8 +- packages/block-editor/src/hooks/utils.js | 34 +- packages/block-editor/src/private-apis.js | 5 +- packages/block-editor/src/store/actions.js | 98 +-- packages/block-editor/src/store/reducer.js | 19 + packages/block-editor/src/store/selectors.js | 32 +- packages/block-editor/src/store/utils.js | 12 - packages/block-library/src/block/edit.js | 3 +- packages/block-library/src/button/edit.js | 74 +- packages/block-library/src/code/transforms.js | 22 +- .../edit/comments-inspector-controls.js | 1 + .../src/cover/edit/inspector-controls.js | 1 + packages/block-library/src/details/edit.js | 1 - .../src/form-submit-button/edit.js | 1 + packages/block-library/src/form/index.js | 2 +- .../block-library/src/gallery/gap-styles.js | 19 +- packages/block-library/src/group/edit.js | 1 + packages/block-library/src/html/transforms.js | 7 +- .../block-library/src/image/deprecated.js | 8 + packages/block-library/src/image/editor.scss | 7 + packages/block-library/src/image/index.php | 7 +- packages/block-library/src/image/style.scss | 31 +- packages/block-library/src/image/view.js | 35 +- packages/block-library/src/missing/block.json | 2 +- .../block-library/src/navigation-link/edit.js | 4 +- .../src/navigation-link/index.php | 57 ++ .../navigation/edit/overlay-menu-preview.js | 2 +- .../block-library/src/navigation/index.php | 439 +----------- .../use-template-part-area-label.js | 6 +- .../__snapshots__/transforms.native.js.snap | 6 + .../src/paragraph/test/transforms.native.js | 1 + packages/block-library/src/pattern/edit.js | 3 +- packages/block-library/src/pattern/index.php | 7 +- .../block-library/src/post-author/edit.js | 1 - .../src/post-featured-image/edit.js | 43 +- .../src/post-featured-image/editor.scss | 19 + .../src/post-template/block.json | 1 - .../block-library/src/post-template/edit.js | 6 +- packages/block-library/src/post-terms/edit.js | 2 - .../src/preformatted/transforms.js | 5 +- .../src/query/edit/query-content.js | 1 + packages/block-library/src/query/index.php | 8 +- packages/block-library/src/quote/block.json | 6 + packages/block-library/src/quote/style.scss | 4 + .../block-library/src/read-more/style.scss | 2 +- .../template-part/edit/advanced-controls.js | 1 + .../src/template-part/edit/index.js | 21 +- .../block-library/src/template-part/index.js | 7 +- .../block-library/src/template-part/index.php | 8 +- .../src/template-part/variations.js | 6 +- packages/blocks/README.md | 1 - .../src/api/raw-handling/paste-handler.js | 31 +- .../api/raw-handling/test/paste-handler.js | 2 - packages/components/CHANGELOG.md | 27 + packages/components/package.json | 1 - packages/components/src/disclosure/index.js | 11 - packages/components/src/disclosure/index.tsx | 44 ++ packages/components/src/disclosure/types.tsx | 10 + packages/components/src/divider/component.tsx | 6 +- .../src/divider/stories/index.story.tsx | 8 + packages/components/src/divider/types.ts | 4 +- .../src/dropdown-menu-v2-ariakit/README.md | 7 - .../src/dropdown-menu-v2-ariakit/index.tsx | 46 +- .../stories/index.story.tsx | 5 +- .../dropdown-menu-v2-ariakit/test/index.tsx | 26 - .../src/dropdown-menu-v2-ariakit/types.ts | 2 +- .../components/src/dropdown-menu/style.scss | 4 + .../components/src/gradient-picker/index.tsx | 2 +- packages/components/src/index.native.js | 1 + .../src/mobile/audio-player/index.native.js | 22 +- .../global-styles-context/utils.native.js | 55 +- .../components/src/radio-group/context.tsx | 18 + packages/components/src/radio-group/index.js | 51 -- packages/components/src/radio-group/index.tsx | 65 ++ .../src/radio-group/radio-context/index.js | 11 - packages/components/src/radio-group/radio.tsx | 55 ++ .../components/src/radio-group/radio/index.js | 40 -- .../src/radio-group/stories/index.story.js | 83 --- .../src/radio-group/stories/index.story.tsx | 90 +++ packages/components/src/radio-group/types.ts | 39 ++ packages/components/src/tabs/README.md | 36 +- .../src/tabs/stories/index.story.tsx | 10 +- packages/components/src/tabs/styles.ts | 16 + packages/components/src/tabs/tab.tsx | 12 +- packages/components/src/tabs/tablist.tsx | 41 +- packages/components/src/tabs/tabpanel.tsx | 47 +- packages/components/src/tabs/test/index.tsx | 87 ++- packages/components/src/tabs/types.ts | 39 +- .../components/src/text-control/index.tsx | 6 +- .../components/src/text-control/style.scss | 5 + packages/components/src/text-control/types.ts | 6 + .../toggle-group-control/component.tsx | 10 +- .../toggle-group-control/styles.ts | 17 +- .../src/toggle-group-control/types.ts | 6 + .../src/toolbar/toolbar-button/style.scss | 5 - packages/core-data/README.md | 26 + packages/core-data/src/actions.js | 16 + packages/core-data/src/entity-provider.js | 9 +- .../core-data/src/queried-data/reducer.js | 6 +- .../core-data/src/queried-data/selectors.js | 4 +- .../src/queried-data/test/reducer.js | 11 + packages/core-data/src/reducer.js | 21 + packages/core-data/src/resolvers.js | 11 + packages/core-data/src/selectors.ts | 22 + .../CHANGELOG.md | 4 + .../create-block-tutorial-template/README.md | 4 +- .../assets/gilbert-color.otf | Bin 626352 -> 0 bytes .../block-templates/edit.js.mustache | 95 ++- .../block-templates/editor.scss.mustache | 13 - .../block-templates/index.js.mustache | 40 +- .../block-templates/render.php.mustache | 35 +- .../block-templates/save.js.mustache | 24 +- .../block-templates/style.scss.mustache | 17 - .../create-block-tutorial-template/index.js | 40 +- .../package.json | 7 +- .../plugin-templates/$slug.php.mustache | 6 +- .../plugin-templates/readme.txt.mustache | 2 +- packages/create-block/README.md | 4 +- packages/create-block/lib/templates.js | 3 +- .../src/components/header/index.js | 29 +- packages/customize-widgets/src/style.scss | 31 - .../data/src/components/use-select/index.js | 9 +- .../e2e-test-utils-playwright/package.json | 3 +- .../src/admin/create-new-post.js | 47 -- .../src/admin/create-new-post.ts | 38 ++ .../src/admin/edit-post.ts | 24 + .../src/admin/index.ts | 13 +- .../src/admin/visit-site-editor.ts | 87 +-- .../editor/click-block-options-menu-item.ts | 5 +- .../src/editor/index.ts | 3 + .../src/editor/set-preferences.ts | 37 + .../src/metrics/index.ts | 107 +++ .../src/page-utils/drag-files.ts | 36 +- .../e2e-test-utils-playwright/src/test.ts | 4 +- .../e2e-test-utils-playwright/src/types.ts | 27 +- .../e2e-tests/plugins/interactive-blocks.php | 4 +- .../container-blocks.test.js.snap | 58 -- .../__snapshots__/cpt-locking.test.js.snap | 147 ---- .../inner-blocks-render-appender.test.js.snap | 25 - .../meta-attribute-block.test.js.snap | 9 - .../__snapshots__/plugins-api.test.js.snap | 7 - .../specs/editor/plugins/annotations.test.js | 189 ------ .../specs/editor/plugins/child-blocks.test.js | 66 -- .../editor/plugins/container-blocks.test.js | 130 ---- .../specs/editor/plugins/cpt-locking.test.js | 251 ------- ...blocks-prioritized-inserter-blocks.test.js | 132 ---- .../inner-blocks-render-appender.test.js | 126 ---- .../plugins/meta-attribute-block.test.js | 100 --- .../specs/editor/plugins/meta-boxes.test.js | 137 ---- .../specs/editor/plugins/plugins-api.test.js | 189 ------ .../editor/various/publish-button.test.js | 46 -- .../header/document-actions/index.js | 11 +- .../components/header/header-toolbar/index.js | 6 +- .../edit-post/src/components/header/index.js | 121 +++- .../src/components/header/style.scss | 33 +- .../components/keyboard-shortcuts/index.js | 9 +- .../src/components/layout/style.scss | 11 - .../components/sidebar/post-schedule/index.js | 1 + .../sidebar/post-schedule/style.scss | 10 +- .../components/sidebar/post-status/index.js | 9 +- .../sidebar/post-template/style.scss | 5 +- .../components/sidebar/post-url/style.scss | 26 - .../sidebar/post-visibility/style.scss | 5 +- .../components/start-page-options/index.js | 23 +- .../src/components/visual-editor/style.scss | 90 --- packages/edit-post/src/style.scss | 1 - packages/edit-site/package.json | 2 +- .../edit-site/src/components/actions/index.js | 118 +++- .../add-new-template/new-template.js | 5 - .../default-block-editor-provider.js | 75 -- .../block-editor-provider/index.js | 29 - .../navigation-block-editor-provider.js | 114 ---- .../test/use-page-content-blocks.js | 116 ---- .../use-page-content-blocks.js | 90 --- .../src/components/block-editor/index.js | 28 - .../src/components/block-editor/style.scss | 87 --- .../block-editor/use-site-editor-settings.js | 157 ++--- .../src/components/dataviews/README.md | 160 +++-- .../src/components/dataviews/add-filter.js | 111 +++ .../src/components/dataviews/dataviews.js | 22 +- .../src/components/dataviews/filters.js | 105 +-- .../src/components/dataviews/in-filter.js | 52 +- .../src/components/dataviews/index.js | 2 +- .../src/components/dataviews/item-actions.js | 115 +++- .../src/components/dataviews/pagination.js | 9 +- .../src/components/dataviews/reset-filters.js | 26 + .../src/components/dataviews/search.js | 1 + .../src/components/dataviews/style.scss | 6 +- .../src/components/dataviews/view-actions.js | 48 +- .../src/components/dataviews/view-list.js | 177 ++++- .../components/dataviews/view-side-by-side.js | 9 + .../edit-site/src/components/editor/index.js | 270 ++++---- .../global-styles-renderer/index.js | 2 +- .../global-styles/screen-revisions/index.js | 77 ++- .../components/global-styles/screen-root.js | 3 +- .../src/components/global-styles/ui.js | 2 +- .../header-edit-mode/document-tools/index.js | 201 ++++++ .../src/components/header-edit-mode/index.js | 270 +++----- .../header-edit-mode/more-menu/site-export.js | 8 +- .../components/header-edit-mode/style.scss | 37 +- .../edit-site/src/components/layout/index.js | 15 +- .../src/components/layout/style.scss | 24 +- .../edit-site/src/components/list/added-by.js | 2 +- .../back-to-page-notification.js | 19 +- .../disable-non-page-content-blocks.js | 71 +- .../src/components/page-main/index.js | 7 +- .../components/page-pages/default-views.js | 60 -- .../src/components/page-pages/index.js | 243 ++++--- .../src/components/page-pages/side-editor.js | 14 + .../src/components/page-patterns/grid-item.js | 6 +- .../components/page-patterns/patterns-list.js | 14 +- .../components/page-patterns/use-patterns.js | 5 +- .../page-templates/dataviews-templates.js | 224 ++++++ .../page-templates/template-actions.js | 209 ++++++ .../sidebar-dataviews/add-new-view.js | 141 ++++ .../custom-dataviews-list.js | 227 +++++++ .../sidebar-dataviews/dataview-item.js | 67 ++ .../sidebar-dataviews/default-views.js | 54 ++ .../src/components/sidebar-dataviews/index.js | 81 +-- .../components/sidebar-dataviews/style.scss | 22 + .../sidebar-edit-mode/default-sidebar.js | 12 +- .../page-panels/edit-template.js | 35 +- .../page-panels/page-summary.js | 3 + .../page-panels/reset-default-template.js | 19 +- .../sidebar-edit-mode/page-panels/style.scss | 5 + .../page-panels/swap-template-button.js | 19 +- .../sidebar-edit-mode/plugin-sidebar/index.js | 12 +- .../sidebar-edit-mode/template-panel/hooks.js | 18 +- .../index.js | 2 +- .../navigation-menu-editor.js | 2 +- .../page-details.js | 21 +- .../use-theme-patterns.js | 4 +- .../edit-site/src/components/sidebar/index.js | 3 +- .../src/components/site-hub/index.js | 17 +- .../start-template-options/index.js | 26 +- .../use-init-edited-entity-from-url.js | 167 +++-- .../convert-to-regular.js | 2 +- .../push-changes-to-global-styles/index.js | 19 +- .../edit-site/src/hooks/template-part-edit.js | 6 +- packages/edit-site/src/index.js | 8 - packages/edit-site/src/store/actions.js | 132 +--- packages/edit-site/src/store/selectors.js | 122 +--- packages/edit-site/src/store/test/actions.js | 148 +--- .../edit-site/src/store/test/selectors.js | 82 --- packages/edit-site/src/style.scss | 1 + packages/edit-site/src/utils/constants.js | 2 +- .../components/header/document-tools/index.js | 130 ++++ .../src/components/header/index.js | 147 +--- .../src/components/header/style.scss | 10 + .../src/components/layout/style.scss | 12 - .../style.scss | 98 --- packages/editor/src/components/index.js | 1 + .../src/components/page-attributes/order.js | 1 + .../components/post-featured-image/index.js | 4 +- .../src/components/post-sync-status/index.js | 7 +- .../components/post-sync-status/style.scss | 5 +- .../editor/src/components/post-title/index.js | 1 - .../src/components/post-title/index.native.js | 12 +- .../src/components/post-url/panel.js} | 32 +- .../editor/src/components/post-url/style.scss | 30 + .../editor/src/components/provider/README.md | 50 ++ .../editor/src/components/provider/index.js | 204 +++++- .../provider/use-block-editor-settings.js | 89 ++- .../use-block-editor-settings.native.js | 4 +- packages/editor/src/private-apis.js | 4 + .../format-library/src/text-color/index.js | 18 - .../src/text-color/index.native.js | 22 - packages/icons/CHANGELOG.md | 4 + packages/icons/src/index.js | 1 + packages/icons/src/library/funnel.js | 12 + .../components/interface-skeleton/index.js | 2 - packages/list-reusable-blocks/package.json | 1 + .../list-reusable-blocks/src/utils/export.js | 4 +- .../list-reusable-blocks/src/utils/file.js | 26 - .../src/components/create-pattern-modal.js | 7 +- .../src/components/patterns-manage-button.js | 6 +- packages/patterns/src/constants.js | 3 +- packages/patterns/src/private-apis.js | 4 +- packages/primitives/src/svg/index.native.js | 1 + packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 6 +- .../__device-tests__/README.md | 36 +- .../gutenberg-editor-drag-and-drop.test.js | 6 +- .../__device-tests__/helpers/utils.js | 39 -- .../react-native-editor/bin/test-e2e-setup.sh | 97 ++- packages/react-native-editor/bin/test-e2e.sh | 12 +- packages/react-native-editor/ios/Podfile.lock | 8 +- packages/react-native-editor/package.json | 2 +- .../reusable-block-convert-button.js | 14 +- packages/rich-text/README.md | 2 - packages/rich-text/src/component/index.js | 50 +- .../src/component/use-copy-handler.js | 7 +- packages/rich-text/src/create.js | 49 +- packages/rich-text/src/index.ts | 1 - .../src/test/__snapshots__/to-dom.js.snap | 6 - packages/rich-text/src/test/helpers/index.js | 19 - packages/rich-text/src/to-html-string.js | 9 +- packages/rich-text/src/to-tree.js | 3 +- packages/scripts/package.json | 4 +- packages/scripts/scripts/test-playwright.js | 6 +- .../block-navigation-link-variations-test.php | 89 +++ phpunit/blocks/render-query-test.php | 89 +-- ...lobal-styles-revisions-controller-test.php | 45 +- phpunit/class-wp-duotone-test.php | 38 ++ ...lass-wp-navigation-block-renderer-test.php | 65 ++ .../directive-processing-test.php | 84 ++- .../docs/create-block/first-block-type.md | 53 +- ...ode-should-paste-plain-text-1-chromium.txt | 3 +- ...preserve-character-newlines-2-chromium.txt | 4 +- ...ve-white-space-when-merging-1-chromium.txt | 5 +- test/e2e/specs/editor/blocks/image.spec.js | 40 +- .../e2e/specs/editor/blocks/paragraph.spec.js | 4 +- test/e2e/specs/editor/blocks/query.spec.js | 5 +- .../blocks/verse-code-preformatted.spec.js | 2 +- test/e2e/specs/editor/local/demo.spec.js | 17 +- .../specs/editor/plugins/annotations.spec.js | 212 ++++++ .../specs/editor/plugins/child-blocks.spec.js | 97 +++ .../editor/plugins/container-blocks.spec.js | 50 ++ ...blocks-prioritized-inserter-blocks.spec.js | 146 ++++ .../inner-blocks-render-appender.spec.js | 129 ++++ .../inner-blocks-template-sync.spec.js | 178 +++++ .../plugins/meta-attribute-block.spec.js | 104 +++ .../specs/editor/plugins/meta-boxes.spec.js | 123 ++++ .../plugins-api-error-boundary.spec.js | 38 ++ .../specs/editor/plugins/plugins-api.spec.js | 233 +++++++ .../editor/plugins/post-type-locking.spec.js | 461 +++++++++++++ ...s-block-selection-is-copied-2-chromium.txt | 4 +- .../editor/various/block-renaming.spec.js | 6 +- ...er-test.spec.js => block-switcher.spec.js} | 0 .../editor/various/draggable-blocks.spec.js | 4 +- .../specs/editor/various/list-view.spec.js | 112 ++- .../various/multi-block-selection.spec.js | 13 +- .../editor/various/navigable-toolbar.spec.js | 238 +++++++ .../various/post-editor-template-mode.spec.js | 6 +- test/e2e/specs/editor/various/preview.spec.js | 5 +- .../editor/various/publish-button.spec.js | 103 +++ .../specs/editor/various/rich-text.spec.js | 2 +- .../editor/various/switch-to-draft.spec.js | 15 +- test/e2e/specs/site-editor/list-view.spec.js | 13 +- test/e2e/specs/site-editor/pages.spec.js | 123 ++++ .../specs/site-editor/template-part.spec.js | 20 +- .../specs/widgets/customizing-widgets.spec.js | 20 +- .../fixtures/blocks/core__form-input.json | 2 +- .../blocks/core__form-input.serialized.html | 2 +- .../fixtures/blocks/core__form.json | 10 +- .../blocks/core__form.serialized.html | 8 +- test/native/setup.js | 1 + .../config/performance-reporter.ts | 3 + test/performance/fixtures/perf-utils.ts | 4 +- test/performance/specs/post-editor.spec.js | 64 +- test/performance/specs/site-editor.spec.js | 101 +-- 479 files changed, 11863 insertions(+), 7748 deletions(-) create mode 100644 docs/getting-started/quick-start-guide.md create mode 100644 lib/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php create mode 100644 lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php create mode 100644 lib/compat/wordpress-6.5/rest-api.php create mode 100644 packages/block-editor/src/components/block-tools/empty-block-inserter.js delete mode 100644 packages/block-editor/src/components/block-tools/selected-block-popover.js create mode 100644 packages/block-editor/src/components/block-tools/selected-block-tools.js create mode 100644 packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js rename packages/block-editor/src/components/inserter/block-patterns-explorer/{explorer.js => index.js} (85%) rename packages/block-editor/src/components/inserter/block-patterns-explorer/{sidebar.js => pattern-explorer-sidebar.js} (100%) rename packages/block-editor/src/components/inserter/block-patterns-explorer/{patterns-list.js => pattern-list.js} (97%) delete mode 100644 packages/block-editor/src/components/inserter/block-patterns-tab.js create mode 100644 packages/block-editor/src/components/inserter/block-patterns-tab/index.js create mode 100644 packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js create mode 100644 packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js rename packages/block-editor/src/components/inserter/{block-patterns-filter.js => block-patterns-tab/patterns-filter.js} (90%) create mode 100644 packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js create mode 100644 packages/block-editor/src/components/inserter/block-patterns-tab/utils.js rename packages/{rich-text/src/component => block-editor/src/components/rich-text/native}/format-edit.js (86%) rename packages/{rich-text/src => block-editor/src/components/rich-text/native}/get-format-colors.native.js (92%) create mode 100644 packages/block-editor/src/components/rich-text/native/index.js rename packages/{rich-text/src/component => block-editor/src/components/rich-text/native}/index.native.js (98%) rename packages/{rich-text/src/component => block-editor/src/components/rich-text/native}/style.native.scss (100%) rename packages/{rich-text/src => block-editor/src/components/rich-text/native}/test/__snapshots__/index.native.js.snap (100%) rename packages/{rich-text/src => block-editor/src/components/rich-text/native}/test/index.native.js (98%) rename packages/{rich-text/src => block-editor/src/components/rich-text/native}/test/performance/rich-text.native.js (94%) rename packages/{rich-text/src/component => block-editor/src/components/rich-text/native}/toolbar-button-with-options.native.js (100%) rename packages/{rich-text/src/component => block-editor/src/components/rich-text/native}/use-format-types.js (97%) delete mode 100644 packages/block-editor/src/store/utils.js delete mode 100644 packages/components/src/disclosure/index.js create mode 100644 packages/components/src/disclosure/index.tsx create mode 100644 packages/components/src/disclosure/types.tsx create mode 100644 packages/components/src/radio-group/context.tsx delete mode 100644 packages/components/src/radio-group/index.js create mode 100644 packages/components/src/radio-group/index.tsx delete mode 100644 packages/components/src/radio-group/radio-context/index.js create mode 100644 packages/components/src/radio-group/radio.tsx delete mode 100644 packages/components/src/radio-group/radio/index.js delete mode 100644 packages/components/src/radio-group/stories/index.story.js create mode 100644 packages/components/src/radio-group/stories/index.story.tsx create mode 100644 packages/components/src/radio-group/types.ts delete mode 100644 packages/create-block-tutorial-template/assets/gilbert-color.otf delete mode 100644 packages/create-block-tutorial-template/block-templates/editor.scss.mustache delete mode 100644 packages/create-block-tutorial-template/block-templates/style.scss.mustache delete mode 100644 packages/e2e-test-utils-playwright/src/admin/create-new-post.js create mode 100644 packages/e2e-test-utils-playwright/src/admin/create-new-post.ts create mode 100644 packages/e2e-test-utils-playwright/src/admin/edit-post.ts create mode 100644 packages/e2e-test-utils-playwright/src/editor/set-preferences.ts delete mode 100644 packages/e2e-tests/specs/editor/plugins/__snapshots__/container-blocks.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/plugins/__snapshots__/cpt-locking.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/plugins/__snapshots__/inner-blocks-render-appender.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/plugins/__snapshots__/meta-attribute-block.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/plugins/__snapshots__/plugins-api.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/plugins/annotations.test.js delete mode 100644 packages/e2e-tests/specs/editor/plugins/child-blocks.test.js delete mode 100644 packages/e2e-tests/specs/editor/plugins/container-blocks.test.js delete mode 100644 packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js delete mode 100644 packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js delete mode 100644 packages/e2e-tests/specs/editor/plugins/inner-blocks-render-appender.test.js delete mode 100644 packages/e2e-tests/specs/editor/plugins/meta-attribute-block.test.js delete mode 100644 packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js delete mode 100644 packages/e2e-tests/specs/editor/plugins/plugins-api.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/publish-button.test.js delete mode 100644 packages/edit-post/src/components/sidebar/post-url/style.scss delete mode 100644 packages/edit-site/src/components/block-editor/block-editor-provider/default-block-editor-provider.js delete mode 100644 packages/edit-site/src/components/block-editor/block-editor-provider/index.js delete mode 100644 packages/edit-site/src/components/block-editor/block-editor-provider/navigation-block-editor-provider.js delete mode 100644 packages/edit-site/src/components/block-editor/block-editor-provider/test/use-page-content-blocks.js delete mode 100644 packages/edit-site/src/components/block-editor/block-editor-provider/use-page-content-blocks.js delete mode 100644 packages/edit-site/src/components/block-editor/index.js create mode 100644 packages/edit-site/src/components/dataviews/add-filter.js create mode 100644 packages/edit-site/src/components/dataviews/reset-filters.js create mode 100644 packages/edit-site/src/components/dataviews/view-side-by-side.js create mode 100644 packages/edit-site/src/components/header-edit-mode/document-tools/index.js delete mode 100644 packages/edit-site/src/components/page-pages/default-views.js create mode 100644 packages/edit-site/src/components/page-pages/side-editor.js create mode 100644 packages/edit-site/src/components/page-templates/dataviews-templates.js create mode 100644 packages/edit-site/src/components/page-templates/template-actions.js create mode 100644 packages/edit-site/src/components/sidebar-dataviews/add-new-view.js create mode 100644 packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js create mode 100644 packages/edit-site/src/components/sidebar-dataviews/dataview-item.js create mode 100644 packages/edit-site/src/components/sidebar-dataviews/default-views.js create mode 100644 packages/edit-site/src/components/sidebar-dataviews/style.scss create mode 100644 packages/edit-widgets/src/components/header/document-tools/index.js rename packages/{edit-post/src/components/sidebar/post-url/index.js => editor/src/components/post-url/panel.js} (68%) create mode 100644 packages/editor/src/components/provider/README.md create mode 100644 packages/icons/src/library/funnel.js create mode 100644 phpunit/blocks/block-navigation-link-variations-test.php create mode 100644 phpunit/class-wp-navigation-block-renderer-test.php create mode 100644 test/e2e/specs/editor/plugins/annotations.spec.js create mode 100644 test/e2e/specs/editor/plugins/child-blocks.spec.js create mode 100644 test/e2e/specs/editor/plugins/container-blocks.spec.js create mode 100644 test/e2e/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.spec.js create mode 100644 test/e2e/specs/editor/plugins/inner-blocks-render-appender.spec.js create mode 100644 test/e2e/specs/editor/plugins/inner-blocks-template-sync.spec.js create mode 100644 test/e2e/specs/editor/plugins/meta-attribute-block.spec.js create mode 100644 test/e2e/specs/editor/plugins/meta-boxes.spec.js create mode 100644 test/e2e/specs/editor/plugins/plugins-api-error-boundary.spec.js create mode 100644 test/e2e/specs/editor/plugins/plugins-api.spec.js create mode 100644 test/e2e/specs/editor/plugins/post-type-locking.spec.js rename test/e2e/specs/editor/various/{block-switcher-test.spec.js => block-switcher.spec.js} (100%) create mode 100644 test/e2e/specs/editor/various/publish-button.spec.js diff --git a/.eslintrc.js b/.eslintrc.js index 8a44b5ef74a1e6..122ec45369c224 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -331,6 +331,7 @@ module.exports = { message: 'Prefer page.locator instead.', }, ], + 'playwright/no-conditional-in-test': 'off', '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', diff --git a/.github/ISSUE_TEMPLATE/Bug_report.yml b/.github/ISSUE_TEMPLATE/Bug_report.yml index ab001b41ff793e..1109056e7e5d56 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/Bug_report.yml @@ -1,5 +1,6 @@ name: Bug report description: Report a bug with the WordPress block editor or Gutenberg plugin +labels: ['[Type] Bug'] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index cfae99f42ff9ea..66bd0943c31b45 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,6 +1,7 @@ --- name: Feature request about: Propose an idea for a feature or an enhancement +labels: "[Type] Enhancement" --- diff --git a/.github/ISSUE_TEMPLATE/New_release.md b/.github/ISSUE_TEMPLATE/New_release.md index d6732a659731f6..629a4dafa5ba56 100644 --- a/.github/ISSUE_TEMPLATE/New_release.md +++ b/.github/ISSUE_TEMPLATE/New_release.md @@ -1,6 +1,7 @@ --- name: Gutenberg Release about: A checklist for the Gutenberg plugin release process +labels: Gutenberg Plugin, [Type] Project Management --- 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. diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index 0921d1f9fc79c1..f0c704e10456c7 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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: token: ${{ secrets.GUTENBERG_TOKEN }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -165,13 +165,13 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: ${{ needs.bump-version.outputs.release_branch || github.ref }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Node.js - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: '.nvmrc' cache: npm @@ -221,7 +221,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 2 ref: ${{ needs.bump-version.outputs.release_branch }} @@ -310,14 +310,14 @@ jobs: if: ${{ endsWith( needs.bump-version.outputs.new_version, '-rc.1' ) }} steps: - name: Checkout (for CLI) - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: path: main ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Checkout (for publishing) - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: path: publish # Later, we switch this branch in the script that publishes packages. @@ -332,7 +332,7 @@ jobs: git config user.email gutenberg@wordpress.org - name: Setup Node.js - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: 'main/.nvmrc' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index d6a9ca83bf3603..3b4d51bddbda0b 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -37,13 +37,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 1 show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Node.js - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: '.nvmrc' cache: npm diff --git a/.github/workflows/check-components-changelog.yml b/.github/workflows/check-components-changelog.yml index 5ab89671c1cf60..fece5aa3a9d9ad 100644 --- a/.github/workflows/check-components-changelog.yml +++ b/.github/workflows/check-components-changelog.yml @@ -20,7 +20,7 @@ jobs: - name: 'Get PR commit count' run: echo "PR_COMMIT_COUNT=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}" - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml index d46a3077ee7c90..0e4325b53f69da 100644 --- a/.github/workflows/create-block.yml +++ b/.github/workflows/create-block.yml @@ -24,7 +24,7 @@ jobs: os: [macos-latest, ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 9bf85a1ac53638..9bd6ba212f0d80 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -27,7 +27,7 @@ jobs: totalParts: [3] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -73,7 +73,7 @@ jobs: totalParts: [4] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -119,7 +119,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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/enforce-pr-labels.yml b/.github/workflows/enforce-pr-labels.yml index 320ef1375029c3..4ef163694b947a 100644 --- a/.github/workflows/enforce-pr-labels.yml +++ b/.github/workflows/enforce-pr-labels.yml @@ -14,5 +14,5 @@ jobs: count: 1 labels: '[Type] Automated Testing, [Type] Breaking Change, [Type] Bug, [Type] Build Tooling, [Type] Code Quality, [Type] Copy, [Type] Developer Documentation, [Type] Enhancement, [Type] Experimental, [Type] Feature, [Type] New API, [Type] Task, [Type] Performance, [Type] Project Management, [Type] Regression, [Type] Security, [Type] WP Core Ticket, Backport from WordPress Core' add_comment: true - message: "**Warning: Type of PR label error**\n\n To merge this PR, it requires {{ errorString }} {{ count }} label indicating the type of PR. Other labels are optional and not being checked here. \n- **Type-related labels to choose from**: {{ provided }}.\n- **Labels found**: {{ applied }}.\n\nRead more about [Type labels in Gutenberg](https://github.com/WordPress/gutenberg/labels?q=type)." + message: "**Warning: Type of PR label mismatch**\n\n To merge this PR, it requires {{ errorString }} {{ count }} label indicating the type of PR. Other labels are optional and not being checked here. \n- **Type-related labels to choose from**: {{ provided }}.\n- **Labels found**: {{ applied }}.\n\nRead more about [Type labels in Gutenberg](https://github.com/WordPress/gutenberg/labels?q=type). Don't worry if you don't have the required permissions to add labels; the PR reviewer should be able to help with the task." exit_type: failure diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index b4012ffc19fd45..bc457758b8385b 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -6,7 +6,7 @@ jobs: name: 'Validation' runs-on: ubuntu-latest steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 875ed28c743aa7..485668a755b8c2 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -32,7 +32,7 @@ jobs: WP_ARTIFACTS_PATH: ${{ github.workspace }}/artifacts steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/php-changes-detection.yml b/.github/workflows/php-changes-detection.yml index 2078bb4a1ddfeb..7e80157d0caf7c 100644 --- a/.github/workflows/php-changes-detection.yml +++ b/.github/workflows/php-changes-detection.yml @@ -10,14 +10,14 @@ jobs: if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} steps: - name: Check out code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Get changed PHP files id: changed-files-php - uses: tj-actions/changed-files@8238a4103220c636f2dad328ead8a7c8dbe316a3 # v39.2.0 + uses: tj-actions/changed-files@25ef3926d147cd02fc7e931c1ef50772bbb0d25d # v40.1.1 with: files: | *.{php} diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml index a93c3ea032fd8a..18bdb63a6c3770 100644 --- a/.github/workflows/publish-npm-packages.yml +++ b/.github/workflows/publish-npm-packages.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout (for CLI) if: ${{ github.event.inputs.release_type != 'wp' }} - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: path: cli ref: trunk @@ -39,7 +39,7 @@ jobs: - name: Checkout (for publishing) if: ${{ github.event.inputs.release_type != 'wp' }} - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: path: publish # Later, we switch this branch in the script that publishes packages. @@ -49,7 +49,7 @@ jobs: - name: Checkout (for publishing WP major version) if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: path: publish ref: wp/${{ github.event.inputs.wp_version }} @@ -67,14 +67,14 @@ jobs: - name: Setup Node.js if: ${{ github.event.inputs.release_type != 'wp' }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: 'cli/.nvmrc' registry-url: 'https://registry.npmjs.org' - name: Setup Node.js (for WP major version) if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: 'publish/.nvmrc' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml index 38cf4c66a503f2..b8154e335776a2 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -15,13 +15,13 @@ 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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Node.js - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version: ${{ matrix.node }} diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index 5f33870be68799..5620e0f66abe5e 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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -47,7 +47,7 @@ jobs: run: npm run native test:e2e:setup - name: Gradle cache - uses: gradle/gradle-build-action@b5126f31dbc19dd434c3269bf8c28c315e121da2 # v2.8.1 + uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0 - name: AVD cache uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index 8f32a9ee9d9dc9..fe3b9b3a3c3a55 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -23,7 +23,7 @@ jobs: native-test-name: [gutenberg-editor-rendering] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index 7fe07bb2ae7bfd..ff8c27b14e39e8 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -22,12 +22,12 @@ jobs: if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Node.js - uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: '.nvmrc' cache: npm diff --git a/.github/workflows/storybook-pages.yml b/.github/workflows/storybook-pages.yml index 32683fac2178c3..5117e2fc9fe6ec 100644 --- a/.github/workflows/storybook-pages.yml +++ b/.github/workflows/storybook-pages.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 7ce44a6931d9e2..65ba01d0b70e89 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -31,7 +31,7 @@ jobs: node: ['16'] steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -111,7 +111,7 @@ jobs: WP_ENV_CORE: ${{ matrix.wordpress == '' && 'WordPress/WordPress' || format( 'https://wordpress.org/wordpress-{0}.zip', matrix.wordpress ) }} steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -126,7 +126,7 @@ jobs: # dependency versions are installed and cached. ## - name: Set up PHP - uses: shivammathur/setup-php@7fdd3ece872ec7ec4c098ae5ab7637d5e0a96067 # v2.26.0 + uses: shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0 # v2.27.1 with: php-version: '${{ matrix.php }}' ini-file: development @@ -221,12 +221,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Set up PHP - uses: shivammathur/setup-php@7fdd3ece872ec7ec4c098ae5ab7637d5e0a96067 # v2.26.0 + uses: shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0 # v2.27.1 with: php-version: '7.4' coverage: none @@ -290,7 +290,7 @@ jobs: if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} steps: - - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index 11ba57b439b593..c10020be057bcb 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -96,7 +96,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: ref: ${{ matrix.branch }} token: ${{ secrets.GUTENBERG_TOKEN }} diff --git a/changelog.txt b/changelog.txt index 4789d83da9768c..2435ab1eceb4ab 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,29 +1,392 @@ == Changelog == -= 17.0.0-rc.1 = += 17.1.0-rc.1 = ## Changelog -### Features +### Enhancements + +#### Block Library +- Navigation block: Fix Inaccurate description of the Show icon button setting. ([55429](https://github.com/WordPress/gutenberg/pull/55429)) +- Query Loop: Add accesibility markup at the end of the loop in all cases. ([55890](https://github.com/WordPress/gutenberg/pull/55890)) +- Template Part: Add fallback to the current theme when not provided. ([55965](https://github.com/WordPress/gutenberg/pull/55965)) +- Update components to use __next40pxDefaultSize. ([56022](https://github.com/WordPress/gutenberg/pull/56022)) + +#### Components +- Tabs: Improve focus behavior. ([55287](https://github.com/WordPress/gutenberg/pull/55287)) +- Tabs: Update subcomponents to accept full HTML element props. ([55860](https://github.com/WordPress/gutenberg/pull/55860)) +- TextControl: Add opt-in prop for 40px default size. ([55471](https://github.com/WordPress/gutenberg/pull/55471)) +- ToggleGroupControl: Add opt-in prop for 40px default size. ([55789](https://github.com/WordPress/gutenberg/pull/55789)) + +#### Patterns +- Move "Manage patterns" below "Detach pattern". ([56018](https://github.com/WordPress/gutenberg/pull/56018)) +- Show theme patterns from directory in site editor. ([55877](https://github.com/WordPress/gutenberg/pull/55877)) + +#### Global Styles +- Global Style Revisions: Ensure consistent back button behaviour. ([55881](https://github.com/WordPress/gutenberg/pull/55881)) +- Global Styles Revisions: More descriptive text timeline. ([55868](https://github.com/WordPress/gutenberg/pull/55868)) +- Global styles revisions: Add route for single styles revisions. ([55827](https://github.com/WordPress/gutenberg/pull/55827)) + +#### Block Locking +- Block Quick Navigation: Truncate text. ([56142](https://github.com/WordPress/gutenberg/pull/56142)) + +#### Block Editor +- Button block: Support double enter to skip to default block. ([56134](https://github.com/WordPress/gutenberg/pull/56134)) + +#### Design Tools +- Add block gap support to Quote block. ([56064](https://github.com/WordPress/gutenberg/pull/56064)) + +#### Post Editor +- "Detach" text change in template options. ([55870](https://github.com/WordPress/gutenberg/pull/55870)) + +#### Site Editor +- Site editor: Add edit page slug field. ([55767](https://github.com/WordPress/gutenberg/pull/55767)) + +#### Interactivity API +- Server directive processing: Process only root blocks. ([55739](https://github.com/WordPress/gutenberg/pull/55739)) + +#### Block settings menu +- Remove the extraneous template part title in replace control. ([55603](https://github.com/WordPress/gutenberg/pull/55603)) + +#### List View +- Add keyboard shortcut to select all blocks. ([54899](https://github.com/WordPress/gutenberg/pull/54899)) + + +### New APIs + +- Download blob: Remove downloadjs dependency. ([56024](https://github.com/WordPress/gutenberg/pull/56024)) + + +### Bug Fixes + +#### Block Library +- Background Image Support: Hide the background image reset button when there's no image. ([55973](https://github.com/WordPress/gutenberg/pull/55973)) +- Background image support: Fix focus loss when resetting background image. ([55984](https://github.com/WordPress/gutenberg/pull/55984)) +- Custom Link: Decode value in URL input field. ([55549](https://github.com/WordPress/gutenberg/pull/55549)) +- Fix lightbox trigger styles. ([55859](https://github.com/WordPress/gutenberg/pull/55859)) +- Form block: Use `type="submit"` for buttons. ([55690](https://github.com/WordPress/gutenberg/pull/55690)) +- Image block: Add check for lightbox values during image block migration. ([56057](https://github.com/WordPress/gutenberg/pull/56057)) +- Image block: Don't show pointer cursor on linked image in the editor. ([55882](https://github.com/WordPress/gutenberg/pull/55882)) +- Lightbox: Fix button misalignment in gallery image. ([56060](https://github.com/WordPress/gutenberg/pull/56060)) +- Lightbox: Fix close button position. ([56125](https://github.com/WordPress/gutenberg/pull/56125)) +- Missing block: Use raw source for originalContent. ([56014](https://github.com/WordPress/gutenberg/pull/56014)) +- Navigation Link block: Register variations on post type / taxonomy registration. ([54801](https://github.com/WordPress/gutenberg/pull/54801)) +- Pattern: Fix regression error in post type templates. ([55858](https://github.com/WordPress/gutenberg/pull/55858)) +- Pattern: Process embeds. ([55979](https://github.com/WordPress/gutenberg/pull/55979)) +- Post feature image block: Wrap images with hrefs in an A tag. ([55498](https://github.com/WordPress/gutenberg/pull/55498)) +- Quote Block: Fix the Quote block layout supports. ([55240](https://github.com/WordPress/gutenberg/pull/55240)) +- Read More block: Reduce text decoration specificity. ([56038](https://github.com/WordPress/gutenberg/pull/56038)) #### Data Views -- Add: Ability to persist dataviews on the database. ([55465](https://github.com/WordPress/gutenberg/pull/55465)) -- DataViews: Extract `search` from filters. ([55722](https://github.com/WordPress/gutenberg/pull/55722)) -- DataViews: Limit users to those who have published pages. ([55455](https://github.com/WordPress/gutenberg/pull/55455)) -- Dataviews: List all pages, not only those with publish and draft statuses. ([55476](https://github.com/WordPress/gutenberg/pull/55476)) +- DataViews: Add missing key to `ResetFilters` component. ([56189](https://github.com/WordPress/gutenberg/pull/56189)) +- DataViews: Fix issue with irrelevant statuses. ([55967](https://github.com/WordPress/gutenberg/pull/55967)) +- DataViews: Fix nested button tags on sidebar. ([56089](https://github.com/WordPress/gutenberg/pull/56089)) +- DataViews: Fix pagination on manual input. ([55940](https://github.com/WordPress/gutenberg/pull/55940)) +- DataViews: Fix spacing issue in top-level bar. ([56151](https://github.com/WordPress/gutenberg/pull/56151)) +- DataViews: Fix status filter upon switching the default views from the sidebar. ([55856](https://github.com/WordPress/gutenberg/pull/55856)) +- DataViews: Make items per page an even number. ([55906](https://github.com/WordPress/gutenberg/pull/55906)) +- DataViews: Make used taxonomy private. ([55918](https://github.com/WordPress/gutenberg/pull/55918)) +- DataViews: Reset pagination upon filter change. ([55797](https://github.com/WordPress/gutenberg/pull/55797)) +- Dataviews: Add a missing icon for the side by side view. ([55925](https://github.com/WordPress/gutenberg/pull/55925)) + +#### Components +- DropdownMenu: Remove extra vertical space around the toggle button. ([56136](https://github.com/WordPress/gutenberg/pull/56136)) +- DropdownMenuV2: Prevent default on Escape key presses. ([55962](https://github.com/WordPress/gutenberg/pull/55962)) +- DropdownMenuV2: Use the `Icon` component to render radio checks. ([55964](https://github.com/WordPress/gutenberg/pull/55964)) + +#### Typography +- Fix fatal error in WP_Fonts_Resolver::Get_settings(). ([55981](https://github.com/WordPress/gutenberg/pull/55981)) +- Font Library: Create fonts dir if a font face needs to use the filesystem. ([56120](https://github.com/WordPress/gutenberg/pull/56120)) +- Font Library: Fix font installation failure. ([55893](https://github.com/WordPress/gutenberg/pull/55893)) + +#### Block Editor +- Iframe: Bubble events from html element instead of body element to fix drag chip positioning. ([56099](https://github.com/WordPress/gutenberg/pull/56099)) +- Post Featured Image: Handling correctly when uploading a file without mime type. ([56133](https://github.com/WordPress/gutenberg/pull/56133)) +- Block Editor: Fix Block editor crash. ([56051](https://github.com/WordPress/gutenberg/pull/56051)) +- Move clientId key to BlockContextualToolbar. ([56008](https://github.com/WordPress/gutenberg/pull/56008)) #### Patterns -- Revert "Patterns: Fix capabilities settings for pattern categories. ([55532](https://github.com/WordPress/gutenberg/pull/55532)) +- Add context for translators to any unclear usage of "synced". ([55935](https://github.com/WordPress/gutenberg/pull/55935)) +- Use existing download function for JSON downloads to fix non-ASCII encoding. ([55912](https://github.com/WordPress/gutenberg/pull/55912)) + +#### Inspector Controls +- Global Styles: Don't show "Apply Styles Globally" button in non-block based themes. ([56033](https://github.com/WordPress/gutenberg/pull/56033)) + +#### Template Editor +- Templates: Update filter to call all of the individual methods. ([55980](https://github.com/WordPress/gutenberg/pull/55980)) + +#### Global Styles +- Global styles revisions: Load unsaved revision item into the revisions preview. ([55880](https://github.com/WordPress/gutenberg/pull/55880)) + +#### Post Editor +- Edit Post: Fix pattern modal reopening when making the title empty again. ([55873](https://github.com/WordPress/gutenberg/pull/55873)) + +#### Data Layer +- Core data: Fix wrong store results when page receives less items that what is stored. ([55832](https://github.com/WordPress/gutenberg/pull/55832)) + + +### Accessibility + +#### Data Views +- DataViews: Add labels to "in-filters". ([56001](https://github.com/WordPress/gutenberg/pull/56001)) +- DataViews: Show actions label. ([56027](https://github.com/WordPress/gutenberg/pull/56027)) + +#### Components +- Fix the image link button pressed state. ([56123](https://github.com/WordPress/gutenberg/pull/56123)) + +#### Block Editor +- Fix mismatching link control action buttons visual order and DOM order. ([56042](https://github.com/WordPress/gutenberg/pull/56042)) +- Escape on Block Toolbar returns focus to Editor Canvas. ([55712](https://github.com/WordPress/gutenberg/pull/55712)) + +#### Site Editor +- Prevent sidebar focus in site editor on small screens. ([55934](https://github.com/WordPress/gutenberg/pull/55934)) + +#### Block Library +- Heading level dropdown: Remove obtrusive tooltips in favor of visible text. ([56035](https://github.com/WordPress/gutenberg/pull/56035)) + + +### Performance + +#### Tooling +- Add a metric to trace template navigation in the site editor. ([55796](https://github.com/WordPress/gutenberg/pull/55796)) + +#### List View +- ListViewBlock: Combine 'useSelect' hooks. ([55889](https://github.com/WordPress/gutenberg/pull/55889)) + +#### Block Editor +- Block Editor: Optimize 'Block Hooks' inspector controls. ([56101](https://github.com/WordPress/gutenberg/pull/56101)) +- Block Editor: Optimize BlockListAppender. ([56116](https://github.com/WordPress/gutenberg/pull/56116)) + +#### Site Editor +- Avoid rerendering the sitehub unnecessarily. ([55818](https://github.com/WordPress/gutenberg/pull/55818)) + +#### Layout +- Block Editor: Optimize layout style renderer subscription. ([55762](https://github.com/WordPress/gutenberg/pull/55762)) + + +### Experiments + +#### Data Views +- DataViews: Add ability to create custom views. ([55773](https://github.com/WordPress/gutenberg/pull/55773)) +- DataViews: Add control to reset all filters at once. ([55955](https://github.com/WordPress/gutenberg/pull/55955)) +- DataViews: Add delete and restore actions. ([55781](https://github.com/WordPress/gutenberg/pull/55781)) +- DataViews: Add initial "Side by side" prototype. ([55343](https://github.com/WordPress/gutenberg/pull/55343)) +- DataViews: Add new page size option. ([56112](https://github.com/WordPress/gutenberg/pull/56112)) +- DataViews: Add rename functionality to custom views. ([55997](https://github.com/WordPress/gutenberg/pull/55997)) +- DataViews: Allow users to add filters dynamically. ([55992](https://github.com/WordPress/gutenberg/pull/55992)) +- DataViews: Update 'All pages' sidebar heading. ([56148](https://github.com/WordPress/gutenberg/pull/56148)) +- DataViews: Update 'View' button. ([56144](https://github.com/WordPress/gutenberg/pull/56144)) +- DataViews: Update `all templates` page. ([55848](https://github.com/WordPress/gutenberg/pull/55848)) +- DataViews: Update author and title fields in template's list. ([56029](https://github.com/WordPress/gutenberg/pull/56029)) +- DataViews: Update filters in view configuration. ([55735](https://github.com/WordPress/gutenberg/pull/55735)) +- DataViews: Add filters to table columns. ([55508](https://github.com/WordPress/gutenberg/pull/55508)) +- DataViews: Add: Ability to delete custom views. ([55924](https://github.com/WordPress/gutenberg/pull/55924)) +- DataViews: Add: Custom views header indication. ([55926](https://github.com/WordPress/gutenberg/pull/55926)) +- DataViews: Remove unnecessary label when no visible filters exist. ([55838](https://github.com/WordPress/gutenberg/pull/55838)) + + +### Documentation + +- Add a first block type page to the platform documentation. ([56109](https://github.com/WordPress/gutenberg/pull/56109)) +- Add new block development "Quick Start Guide" and update the `create-block-tutorial-template`. ([56056](https://github.com/WordPress/gutenberg/pull/56056)) +- Clean up DataViews docs: `filter.id` is not used. ([55833](https://github.com/WordPress/gutenberg/pull/55833)) +- DataViews: Document `enableSorting` and `enableHiding`. ([55988](https://github.com/WordPress/gutenberg/pull/55988)) +- DataViews: Document actions. ([55959](https://github.com/WordPress/gutenberg/pull/55959)) +- Doc: Corrected + updated links. ([56084](https://github.com/WordPress/gutenberg/pull/56084)) +- Doc: Fixes wrong link in #56084. ([56106](https://github.com/WordPress/gutenberg/pull/56106)) +- Docs: Changes imports from `wp.editor` to `wp.blockEditor` for PlainText and RichText. ([55841](https://github.com/WordPress/gutenberg/pull/55841)) +- Fix formatting issue in the "Get started with create-block" doc. ([55872](https://github.com/WordPress/gutenberg/pull/55872)) +- Fix: 404 Link on git workflow docs. ([55897](https://github.com/WordPress/gutenberg/pull/55897)) +- Fix: 404 link in get-started-with-create-block docs. ([55932](https://github.com/WordPress/gutenberg/pull/55932)) +- Fix: Create meta block link in block attributes documentation. ([55804](https://github.com/WordPress/gutenberg/pull/55804)) +- Fix: Filter duotone link on block-supports documentation. ([55896](https://github.com/WordPress/gutenberg/pull/55896)) +- Fix: Two invalid links on docs/contributors/documentation/README.md. ([55843](https://github.com/WordPress/gutenberg/pull/55843)) +- New additional resource for wp-env. ([55987](https://github.com/WordPress/gutenberg/pull/55987)) +- Update documentation to clarify workflow branch for release package publishing. ([56183](https://github.com/WordPress/gutenberg/pull/56183)) +- Update jest links to the new site. ([55802](https://github.com/WordPress/gutenberg/pull/55802)) + + +### Code Quality + +- Block lib: Remove multiline=false (deprecated). ([56113](https://github.com/WordPress/gutenberg/pull/56113)) +- Delete unused `SelectedBlockPopover` component. ([55821](https://github.com/WordPress/gutenberg/pull/55821)) +- Fix: Remove unrequired nullish coalescing. ([55854](https://github.com/WordPress/gutenberg/pull/55854)) +- Fix: Use of integer value in a conditional rendering condition on Gradients. ([55855](https://github.com/WordPress/gutenberg/pull/55855)) +- Give nice unique names to block controls HOCs. ([55795](https://github.com/WordPress/gutenberg/pull/55795)) +- Migrating `PatternTransformationsMenu`. ([56122](https://github.com/WordPress/gutenberg/pull/56122)) +- Migrating block inserter media tab components. ([56195](https://github.com/WordPress/gutenberg/pull/56195)) +- Move document tools motion to header-edit-mode layout level. ([55904](https://github.com/WordPress/gutenberg/pull/55904)) +- Only render block toolbar if blockType has value. ([55861](https://github.com/WordPress/gutenberg/pull/55861)) +- Refactor Edit Widgets Document Tools Navigation to own component. ([55778](https://github.com/WordPress/gutenberg/pull/55778)) +- Refactor Selected Block Tools. ([55737](https://github.com/WordPress/gutenberg/pull/55737)) +- Refactor Site Editor Document Tools Navigation to own component. ([55770](https://github.com/WordPress/gutenberg/pull/55770)) +- Remove BlockStyles.Slot empty component. ([55991](https://github.com/WordPress/gutenberg/pull/55991)) +- Remove obsolete `queryContext`. ([56034](https://github.com/WordPress/gutenberg/pull/56034)) +- Remove unnecessary empty className. ([55998](https://github.com/WordPress/gutenberg/pull/55998)) +- Rename Unforward to Unforwarded and export the named const. ([55820](https://github.com/WordPress/gutenberg/pull/55820)) +- Render Selected Block Tools in Header when using Top Toolbar. ([55787](https://github.com/WordPress/gutenberg/pull/55787)) +- Reusable Blocks: Unlock a private hook and a component at the file level. ([55809](https://github.com/WordPress/gutenberg/pull/55809)) +- Server directive processing: Improve how block references are saved. ([56107](https://github.com/WordPress/gutenberg/pull/56107)) +- Share the editor settings between the post and site editors. ([55970](https://github.com/WordPress/gutenberg/pull/55970)) +- Site Editor: Fix deprecation console error in top toolbar. ([55678](https://github.com/WordPress/gutenberg/pull/55678)) +- Site Editor: Unlock global styles' private hooks at the file level. ([55800](https://github.com/WordPress/gutenberg/pull/55800)) +- Site Editor: Update edited entity sync logic. ([55928](https://github.com/WordPress/gutenberg/pull/55928)) +- Site Editor: Use EditorProvider instead of custom logic. ([56000](https://github.com/WordPress/gutenberg/pull/56000)) +- SiteEditor: Optimize BackToPageNotification component. ([56102](https://github.com/WordPress/gutenberg/pull/56102)) +- SiteEditor: Refactor disable non page content blocks. ([56103](https://github.com/WordPress/gutenberg/pull/56103)) +- Unify the PageUrl and PageSlug components between site and post editors. ([56203](https://github.com/WordPress/gutenberg/pull/56203)) + +#### Data Views +- DataViews: Fix translatable string. ([56075](https://github.com/WordPress/gutenberg/pull/56075)) +- DataViews: Remove `filter.name`. ([55834](https://github.com/WordPress/gutenberg/pull/55834)) +- DataViews: Remove reset values from filters. ([55839](https://github.com/WordPress/gutenberg/pull/55839)) +- DataViews: Remove unnecessary `sortingFn` prop from field description. ([55989](https://github.com/WordPress/gutenberg/pull/55989)) +- DataViews: Simplify filters API. ([55917](https://github.com/WordPress/gutenberg/pull/55917)) +- DataViews: Update actions API. ([56026](https://github.com/WordPress/gutenberg/pull/56026)) + +#### Block Editor +- Rich text: Remove preserveWhiteSpace serialisation differences. ([55999](https://github.com/WordPress/gutenberg/pull/55999)) +- Rich text: highlight format: Gracefully handle old span format. ([56071](https://github.com/WordPress/gutenberg/pull/56071)) +- Update Link Control labels to use gray-900. ([55867](https://github.com/WordPress/gutenberg/pull/55867)) + +#### Components +- `DisclosureContent`: Migrate from `reakit` to `@ariakit/react`. ([55639](https://github.com/WordPress/gutenberg/pull/55639)) +- `Divider`: Migrate from `reakit` to `@ariakit/react`. ([55622](https://github.com/WordPress/gutenberg/pull/55622)) +- `RadioGroup`: Migrate from `reakit` to `ariakit`. ([55580](https://github.com/WordPress/gutenberg/pull/55580)) + +#### Site Editor +- Core Data: Move the template lookup to core-data selectors/resolvers. ([55883](https://github.com/WordPress/gutenberg/pull/55883)) +- Don't use 'useEntityRecord' to only dispatch actions. ([56076](https://github.com/WordPress/gutenberg/pull/56076)) + +#### Block Library +- Navigation: Refactor the PHP render function to make it easier to make changes in the future. ([55605](https://github.com/WordPress/gutenberg/pull/55605)) +- Update `blockEditor.__unstableCanInsertBlockType` hook namespace. ([55845](https://github.com/WordPress/gutenberg/pull/55845)) + +#### Data Layer +- Data: Fix ESLint warnings for the 'useSelect' hook. ([55916](https://github.com/WordPress/gutenberg/pull/55916)) + +#### Post Editor +- Edit Post: Use a single 'useSelect' hook for getting selectors. ([55902](https://github.com/WordPress/gutenberg/pull/55902)) + +#### Colors +- Add Unit testing for duotone enhanced pagination. ([55542](https://github.com/WordPress/gutenberg/pull/55542)) + +#### Patterns +- Split up the block editor inserter patterns tab into separate component files. ([55315](https://github.com/WordPress/gutenberg/pull/55315)) + +#### Design Tools +- Block styles: Remove __unstableElementContext in favour of useStyleOverride. ([54493](https://github.com/WordPress/gutenberg/pull/54493)) + + +### Tools + +- Issue Templates: Add default type labels to issue templates. ([55826](https://github.com/WordPress/gutenberg/pull/55826)) +- Label enforcer: Make the warning message less scary for new contributors. ([55900](https://github.com/WordPress/gutenberg/pull/55900)) +- Quote feature request label. ([55862](https://github.com/WordPress/gutenberg/pull/55862)) + +#### Testing +- Disable 'no-conditional-in-test' ESLint rule for Playwright. ([56088](https://github.com/WordPress/gutenberg/pull/56088)) +- Fix 'Block Switcher' test file name for Playwright end-to-end tests. ([55840](https://github.com/WordPress/gutenberg/pull/55840)) +- Fix flaky 'Meta boxes' end-to-end tests. ([56083](https://github.com/WordPress/gutenberg/pull/56083)) +- Migrate 'CPT locking' end-to-end tests to Playwright. ([55929](https://github.com/WordPress/gutenberg/pull/55929)) +- Migrate 'Meta boxes' end-to-end tests to Playwright. ([55915](https://github.com/WordPress/gutenberg/pull/55915)) +- Migrate 'Plugins API' end-to-end tests to Playwright. ([55958](https://github.com/WordPress/gutenberg/pull/55958)) +- Migrate 'annotations' end-to-end tests to Playwright. ([55966](https://github.com/WordPress/gutenberg/pull/55966)) +- Migrate 'container blocks' end-to-end tests to Playwright. ([56141](https://github.com/WordPress/gutenberg/pull/56141)) +- Migrate 'inner-blocks-prioritized-inserter-blocks' end-to-end tests to Playwright. ([55828](https://github.com/WordPress/gutenberg/pull/55828)) +- Migrate 'inner-blocks-render-appender' end-to-end tests to Playwright. ([55814](https://github.com/WordPress/gutenberg/pull/55814)) +- Migrate 'meta-attribute-block' end-to-end tests to Playwright. ([55830](https://github.com/WordPress/gutenberg/pull/55830)) +- Migrate Child Block Test to Playwright. ([55199](https://github.com/WordPress/gutenberg/pull/55199)) +- Migrate flaky PostPublishButton end-to-end tests to Playwright. ([52285](https://github.com/WordPress/gutenberg/pull/52285)) +- Perf Tests: Stabilise the Site Editor metrics. ([55922](https://github.com/WordPress/gutenberg/pull/55922)) +- Playwright Utils: Fix 'clickBlockOptionsMenuItem' helper. ([55923](https://github.com/WordPress/gutenberg/pull/55923)) +- Query block enhanced pagination: Simplify test setup. ([55805](https://github.com/WordPress/gutenberg/pull/55805)) +- Site editor template preview: Add end-to-end test and aria-pressed attribute to template preview toggle. ([56096](https://github.com/WordPress/gutenberg/pull/56096)) +- Upgrade Playwright to 1.39.0. ([54051](https://github.com/WordPress/gutenberg/pull/54051)) +- end-to-end Utils: Add setPreferences and editPost utils. ([55099](https://github.com/WordPress/gutenberg/pull/55099)) +- end-to-end Utils: Add support for web-vitals.js. ([55660](https://github.com/WordPress/gutenberg/pull/55660)) + +#### Build Tooling +- Package `@ariakit/test` should be a dev dependency. ([56091](https://github.com/WordPress/gutenberg/pull/56091)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @joanrodas: Update Link Control labels to use gray-900. ([55867](https://github.com/WordPress/gutenberg/pull/55867)) +- @JorgeVilchez95: "Detach" text change in template options. ([55870](https://github.com/WordPress/gutenberg/pull/55870)) +- @sacerro: Styles: More descriptive text for revisions timeline. ([55868](https://github.com/WordPress/gutenberg/pull/55868)) + + +## Contributors + +The following contributors merged PRs in this release: + +@afercia @andrewhayward @andrewserong @anomiex @anton-vlasenko @aristath @artemiomorales @bph @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @DAreRodz @dcalhoun @dsas @ellatrix @flootr @fluiddot @gaambo @glendaviesnz @gziolo @jameskoster @jeryj @jhnstn @joanrodas @jorgefilipecosta @JorgeVilchez95 @jsnajdr @juanmaguitar @kevin940726 @Mamaduka @masteradhoc @matiasbenedetto @ndiego @ntsekouras @oandregal @peterwilsoncc @pooja-muchandikar @priethor @ramonjd @renatho @richtabor @sacerro @scruffian @shimotmk @SiobhyB @Soean @swissspidy @t-hamano @talldan @tellthemachines @torounit @tyxla @WunderBart @youknowriad + + += 17.0.2 = + + + +## Changelog + +### Bug Fixes + +#### Typography +- Fix another fatal error in WP_Fonts_Resolver::Get_settings(). ([56067](https://github.com/WordPress/gutenberg/pull/56067)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@arthur791004 + + += 17.0.1 = + +## Changelog + +### Bug Fixes + +- Fix a fatal error in `WP_Fonts_Resolver::get_settings()`. ([55981](https://github.com/WordPress/gutenberg/pull/55981)) + +## Contributors + +@anton-vlasenko + + += 16.9.1 = + +Fixes a PHP fatal error in `WP_Fonts_Resolver::get_settings()`: https://github.com/WordPress/gutenberg/pull/55981 + + += 17.0.0 = + + + +## Changelog ### Enhancements -- Add blog icon to the blog home page in LinkControl search results. ([55610](https://github.com/WordPress/gutenberg/pull/55610)) -- Add: Default readonly views to dataviews. ([55740](https://github.com/WordPress/gutenberg/pull/55740)) -- Add: Possibility to tree shake DataViewsProvider. ([55641](https://github.com/WordPress/gutenberg/pull/55641)) +#### Link Control +- Add blog icon to the blog home page in LinkControl search results. ([55610](https://github.com/WordPress/gutenberg/pull/55610)) +- Add home icon to the front page in LinkControl results. ([55606](https://github.com/WordPress/gutenberg/pull/55606)) + +#### Block Theme Preview - Block Theme Preview: Display loading state when activating. ([55658](https://github.com/WordPress/gutenberg/pull/55658)) + +#### Block Library +- Template Part block: Use `_build_block_template_result_from_post`. ([55811](https://github.com/WordPress/gutenberg/pull/55811)) + +#### Block Toolbar - Improve toolbar button focus visual. ([55523](https://github.com/WordPress/gutenberg/pull/55523)) #### Components @@ -31,37 +394,35 @@ - Update InputControl and SelectControl default heights. ([55490](https://github.com/WordPress/gutenberg/pull/55490)) - Update ariakit to version 0.3.5. ([55365](https://github.com/WordPress/gutenberg/pull/55365)) -#### Block Library -- Allow using groups and columns inside the experimental form block. ([55758](https://github.com/WordPress/gutenberg/pull/55758)) - #### Block Editor - Adjust the padding of the category item to make it visually balanced. ([55598](https://github.com/WordPress/gutenberg/pull/55598)) -#### Data Views -- [Data views]: Update icons and design tweaks. ([55391](https://github.com/WordPress/gutenberg/pull/55391)) - #### Patterns - Suggest commands when editing pattern in site editor. ([55332](https://github.com/WordPress/gutenberg/pull/55332)) - ### Bug Fixes +- Fix autocomplete trigger character detection. ([55301](https://github.com/WordPress/gutenberg/pull/55301)) +- Fix typo in `FontCollection`. ([55439](https://github.com/WordPress/gutenberg/pull/55439)) + +#### Block Editor + - Block Editor: Avoid rendering empty Slot for block alignments. ([55689](https://github.com/WordPress/gutenberg/pull/55689)) - Block Example: Avoid a crash when block is not registered. ([55686](https://github.com/WordPress/gutenberg/pull/55686)) + +#### Command Palette - Command Palette: Fix a crash when transform to a block without icon. ([55676](https://github.com/WordPress/gutenberg/pull/55676)) -- Ensure Term Description block is registered in core. ([55669](https://github.com/WordPress/gutenberg/pull/55669)) -- Fix autocomplete trigger character detection. ([55301](https://github.com/WordPress/gutenberg/pull/55301)) -- Fix typo in `FontCollection`. ([55439](https://github.com/WordPress/gutenberg/pull/55439)) -- Fix: Add __next40pxDefaultSize to author select in PostAuthor. ([55597](https://github.com/WordPress/gutenberg/pull/55597)) -- Fix: Add home icon to the front page in LinkControl results. ([55606](https://github.com/WordPress/gutenberg/pull/55606)) -- Fix: Don't register dataviews postype and taxonomy if experiment is disabled. ([55743](https://github.com/WordPress/gutenberg/pull/55743)) + +#### Link Control - Fix: LinkControl on site editor does not show font page and post page indication. ([55694](https://github.com/WordPress/gutenberg/pull/55694)) - Fix: LinkControl on widgets editor does not show font page and post page indication. ([55732](https://github.com/WordPress/gutenberg/pull/55732)) -- Fix: Post terms block: Missing options for prefix and suffix when the…. ([55726](https://github.com/WordPress/gutenberg/pull/55726)) + +#### Interactivity API - Fix: Update page title when using enhanced pagination in query loop. ([55446](https://github.com/WordPress/gutenberg/pull/55446)) -- Use PostCSS + PostCSS plugins for style transformation. ([49521](https://github.com/WordPress/gutenberg/pull/49521)) #### Block Library +- Fix: Post terms block: Missing options for prefix and suffix when the…. ([55726](https://github.com/WordPress/gutenberg/pull/55726)) +- Ensure Term Description block is registered in core. ([55669](https://github.com/WordPress/gutenberg/pull/55669)) - Fix #55679 missing space in the string. ([55682](https://github.com/WordPress/gutenberg/pull/55682)) - Image block: Wrap images with hrefs in an A tag. ([55470](https://github.com/WordPress/gutenberg/pull/55470)) - Image: Improve focus management in lightbox. ([55428](https://github.com/WordPress/gutenberg/pull/55428)) @@ -75,33 +436,28 @@ #### Patterns - Fix bug with authors and contributors not seeing user pattern categories. ([55553](https://github.com/WordPress/gutenberg/pull/55553)) -- Fix capabilities settings for pattern categories. ([55379](https://github.com/WordPress/gutenberg/pull/55379)) - Fix pattern category renaming causing potential duplicate categories. ([55607](https://github.com/WordPress/gutenberg/pull/55607)) #### Post Editor - Edit Post: Fix revision button misalignment. ([55659](https://github.com/WordPress/gutenberg/pull/55659)) - Preferences modal: Fix the position of sticky heading when blocks are hidden. ([55456](https://github.com/WordPress/gutenberg/pull/55456)) +- Fix: Add __next40pxDefaultSize to author select in PostAuthor. ([55597](https://github.com/WordPress/gutenberg/pull/55597)) #### Site Editor - Fix the 'Slug' display for draft pages. ([55774](https://github.com/WordPress/gutenberg/pull/55774)) #### Data Views -- DataViews: List all users in author filter. ([55723](https://github.com/WordPress/gutenberg/pull/55723)) +- List all users in author filter. ([55723](https://github.com/WordPress/gutenberg/pull/55723)) +- Don't register dataviews postype and taxonomy if experiment is disabled. ([55743](https://github.com/WordPress/gutenberg/pull/55743)) #### Design Tools -- Fix: Remove default dimensions controls from core/avatar. ([55596](https://github.com/WordPress/gutenberg/pull/55596)) +- Remove default dimensions controls from core/avatar. ([55596](https://github.com/WordPress/gutenberg/pull/55596)) -#### Collaborative Editing -- Remove dangling comma causing PHPUnit failures. ([55494](https://github.com/WordPress/gutenberg/pull/55494)) #### Interactivity API - Fix server processing of an empty `data-wp-context` directive. ([55482](https://github.com/WordPress/gutenberg/pull/55482)) - -#### Layout -- Make layout support compatible with enhanced pagination. ([55416](https://github.com/WordPress/gutenberg/pull/55416)) - -#### Colors - Make duotone support compatible with enhanced pagination. ([55415](https://github.com/WordPress/gutenberg/pull/55415)) +- Make layout support compatible with enhanced pagination. ([55416](https://github.com/WordPress/gutenberg/pull/55416)) #### Global Styles - Fix duotone not showing in site editor style block level styles. ([55361](https://github.com/WordPress/gutenberg/pull/55361)) @@ -134,11 +490,23 @@ #### Data Views - Trash Data View: Use trash icon. ([55771](https://github.com/WordPress/gutenberg/pull/55771)) -- [Data views]: Add quick actions. ([55488](https://github.com/WordPress/gutenberg/pull/55488)) +- Add quick actions. ([55488](https://github.com/WordPress/gutenberg/pull/55488)) +- Update icons and design tweaks. ([55391](https://github.com/WordPress/gutenberg/pull/55391)) +- Add ability to persist dataviews on the database. ([55465](https://github.com/WordPress/gutenberg/pull/55465)) +- Extract `search` from filters. ([55722](https://github.com/WordPress/gutenberg/pull/55722)) +- Limit users to those who have published pages. ([55455](https://github.com/WordPress/gutenberg/pull/55455)) +- List all pages, not only those with publish and draft statuses. ([55476](https://github.com/WordPress/gutenberg/pull/55476)) +- Add: Default readonly views to dataviews. ([55740](https://github.com/WordPress/gutenberg/pull/55740)) +- Add: Possibility to tree shake DataViewsProvider. ([55641](https://github.com/WordPress/gutenberg/pull/55641)) -#### Block Library -- Form Blocks: Capitalize title and end the description with a period. ([55728](https://github.com/WordPress/gutenberg/pull/55728)) +#### Form block +- Blocks: Capitalize title and end the description with a period. ([55728](https://github.com/WordPress/gutenberg/pull/55728)) +- Update copy. ([55468](https://github.com/WordPress/gutenberg/pull/55468)) +- Allow using groups and columns inside the experimental form block. ([55758](https://github.com/WordPress/gutenberg/pull/55758)) +#### Collaborative Editing +- Remove dangling comma causing PHPUnit failures. ([55494](https://github.com/WordPress/gutenberg/pull/55494)) +- [Try] HTTP based PHP signaling server for colaborative editing. ([53922](https://github.com/WordPress/gutenberg/pull/53922)) ### Documentation @@ -154,7 +522,7 @@ ### Code Quality - +- Use PostCSS + PostCSS plugins for style transformation. ([49521](https://github.com/WordPress/gutenberg/pull/49521)) - CS: Remove redundant ignore annotations. ([55615](https://github.com/WordPress/gutenberg/pull/55615)) - Chore: Fix: Potential access to properties of undefined object. ([55697](https://github.com/WordPress/gutenberg/pull/55697)) - Fix: Add missing defaultStatuses on PagePages. ([55761](https://github.com/WordPress/gutenberg/pull/55761)) @@ -162,13 +530,14 @@ - Update: Code quality: Use for each instead of map. ([55798](https://github.com/WordPress/gutenberg/pull/55798)) - Update: Remove path awareness from data-views specific code. ([55695](https://github.com/WordPress/gutenberg/pull/55695)) - Update: Remove useless self assignment. ([55696](https://github.com/WordPress/gutenberg/pull/55696)) -- chore: Fix: Remove unused file dataview context. ([55775](https://github.com/WordPress/gutenberg/pull/55775)) +- Backport updates from Core during the 6.4 release cycle. ([55703](https://github.com/WordPress/gutenberg/pull/55703)) #### Data Views - DataViews: Iterate on filter's API. ([55440](https://github.com/WordPress/gutenberg/pull/55440)) - DataViews: Pass `search` filter as global, unattached from fields. ([55475](https://github.com/WordPress/gutenberg/pull/55475)) - DataViews: Remove string from pages sidebar. ([55510](https://github.com/WordPress/gutenberg/pull/55510)) - Rename `TextFilter` to `Search`. ([55731](https://github.com/WordPress/gutenberg/pull/55731)) +- Remove unused file dataview context. ([55775](https://github.com/WordPress/gutenberg/pull/55775)) #### Block Library - Pattern: Unlock the private hook outside the component. ([55792](https://github.com/WordPress/gutenberg/pull/55792)) @@ -181,6 +550,7 @@ - Replace 'npx' with 'node' on test-playwright.js file'. ([55616](https://github.com/WordPress/gutenberg/pull/55616)) - wp-env: Fix errors which prevent it from working without internet. ([53547](https://github.com/WordPress/gutenberg/pull/53547)) +- [create-block] Add ABSPATH check. ([55533](https://github.com/WordPress/gutenberg/pull/55533)) #### Testing - E2E: Revert typing delay on the autocomplete mentions test. ([55132](https://github.com/WordPress/gutenberg/pull/55132)) @@ -191,18 +561,6 @@ - Scripts: Fix default Playwright configuration. ([55453](https://github.com/WordPress/gutenberg/pull/55453)) -### Various - -- Form: Update copy. ([55468](https://github.com/WordPress/gutenberg/pull/55468)) -- [create-block] Add ABSPATH check. ([55533](https://github.com/WordPress/gutenberg/pull/55533)) - -#### HTML API -- Backport updates from Core during the 6.4 release cycle. ([55703](https://github.com/WordPress/gutenberg/pull/55703)) - -#### Collaborative Editing -- [Try] HTTP based PHP signaling server for colaborative editing. ([53922](https://github.com/WordPress/gutenberg/pull/53922)) - - ## First time contributors The following PRs were merged by first time contributors: diff --git a/docs/contributors/code/git-workflow.md b/docs/contributors/code/git-workflow.md index 4e4e886f670a80..eeeb68282c1cdf 100644 --- a/docs/contributors/code/git-workflow.md +++ b/docs/contributors/code/git-workflow.md @@ -23,7 +23,7 @@ See the [repository management document](/docs/contributors/repository-managemen ## Git Workflow Walkthrough -The workflow for code and documentation is the same, since both are managed in GitHub. You can watch a [video walk-through of contributing documentation](https://wordpress.tv/2020/09/02/marcus-kazmierczak-contribute-developer-documentation-to-gutenberg/) and the accompanying [slides for contributing to Gutenberg](https://mkaz.blog/wordpress/contribute-documentation-to-gutenberg/). +The workflow for code and documentation is the same, since both are managed in GitHub. You can watch a [video walk-through of contributing documentation](https://wordpress.tv/2020/09/02/marcus-kazmierczak-contribute-developer-documentation-to-gutenberg/) and the accompanying [tutorial for contributing to Gutenberg](https://mkaz.blog/wordpress/contribute-developer-documentation-to-gutenberg/). Here is a visual overview of the Git workflow: diff --git a/docs/contributors/code/react-native/getting-started-react-native.md b/docs/contributors/code/react-native/getting-started-react-native.md index 55260a9c841546..7b4dcca98027d0 100644 --- a/docs/contributors/code/react-native/getting-started-react-native.md +++ b/docs/contributors/code/react-native/getting-started-react-native.md @@ -139,7 +139,7 @@ Then, open `chrome://inspect` in Chrome to attach the debugger (look into the "R ## Writing and Running Unit Tests -This project is set up to use [jest](https://facebook.github.io/jest/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test.js` extension to have the files loaded by jest. See an example test [here](https://github.com/WordPress/gutenberg/blob/HEAD/packages/react-native-editor/src/test/api-fetch-setup.test.js). The [jest documentation](https://facebook.github.io/jest/docs/en/getting-started.html) is also a wonderful resource, as is the [React Native testing tutorial](https://facebook.github.io/jest/docs/en/tutorial-react-native.html). +This project is set up to use [jest](https://jestjs.io/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test.js` extension to have the files loaded by jest. See an example test [here](https://github.com/WordPress/gutenberg/blob/HEAD/packages/react-native-editor/src/test/api-fetch-setup.test.js). The [jest documentation](https://jestjs.io/docs/getting-started) is also a wonderful resource, as is the [React Native testing tutorial](https://jestjs.io/docs/tutorial-react-native). ## End-to-End Tests diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index 83868f95ce184d..8c8ed3ff3c334e 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -422,7 +422,7 @@ Now, the `wp/X.Y` branch is ready for publishing npm packages. In order to start ![Run workflow dropdown for npm publishing](https://developer.wordpress.org/files/2023/07/image-2.png) -To publish packages to npm for the WordPress major release, select `wp` from the "Release type" dropdown and enter `X.Y` (example `5.2`) in the "WordPress major release" input field. Finally, press the green "Run workflow" button. It triggers the npm publishing job, and this needs to be approved by a Gutenberg Core team member. Locate the ["Publish npm packages" action](https://github.com/WordPress/gutenberg/actions/workflows/publish-npm-packages.yml) for the current publishing, and have it [approved](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments#approving-or-rejecting-a-job). +To publish packages to npm for the WordPress major release, select `trunk` as the branch to run the workflow from (this means that the script used to run the workflow comes from the trunk branch, though the packages themselves will published from the release branch as long as the correct "Release type" is selected below), then select `wp` from the "Release type" dropdown and enter `X.Y` (example `5.2`) in the "WordPress major release" input field. Finally, press the green "Run workflow" button. It triggers the npm publishing job, and this needs to be approved by a Gutenberg Core team member. Locate the ["Publish npm packages" action](https://github.com/WordPress/gutenberg/actions/workflows/publish-npm-packages.yml) for the current publishing, and have it [approved](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments#approving-or-rejecting-a-job). For the record, the manual process would look like the following: diff --git a/docs/contributors/documentation/README.md b/docs/contributors/documentation/README.md index a862592ab318ae..397e7ad1e140c9 100644 --- a/docs/contributors/documentation/README.md +++ b/docs/contributors/documentation/README.md @@ -25,14 +25,14 @@ The block editor handbook is a mix of markdown files in the `/docs/` directory o An automated job publishes the docs every 15 minutes to the [block editor handbook site](https://developer.wordpress.org/block-editor/). -See [the Git Workflow](/docs/contributors/code/git-workflow.md) documentation for how to use git to deploy changes using pull requests. Additionally, see the [video walk-through](https://wordpress.tv/2020/09/02/marcus-kazmierczak-contribute-developer-documentation-to-gutenberg/) and the accompanying [slides for contributing documentation to Gutenberg](https://mkaz.blog/wordpress/contribute-documentation-to-gutenberg/). +See [the Git Workflow](/docs/contributors/code/git-workflow.md) documentation for how to use git to deploy changes using pull requests. Additionally, see the [video walk-through](https://wordpress.tv/2020/09/02/marcus-kazmierczak-contribute-developer-documentation-to-gutenberg/) and the accompanying [slides for contributing documentation to Gutenberg](https://mkaz.blog/wordpress/contribute-developer-documentation-to-gutenberg/). ### Handbook structure The handbook is organized into four sections based on the functional types of documents. [The Documentation System](https://documentation.divio.com/) does a great job explaining the needs and functions of each type, but in short they are: - **Getting started tutorials** - full lessons that take learners step by step to complete an objective, for example the [create a block tutorial](/docs/getting-started/create-block/README.md). -- **How to guides** - short lessons specific to completing a small specific task, for example [how to add a button to the block toolbar](/docshow-to-guides/format-api/README.md). +- **How to guides** - short lessons specific to completing a small specific task, for example [how to add a button to the block toolbar](/docs/how-to-guides/format-api.md). - **Reference guides** - API documentation, purely functional descriptions, - **Explanations** - longer documentation focused on learning, not a specific task. diff --git a/docs/getting-started/create-block/README.md b/docs/getting-started/create-block/README.md index 2b87f09f1f903c..22a28560c76a81 100644 --- a/docs/getting-started/create-block/README.md +++ b/docs/getting-started/create-block/README.md @@ -8,24 +8,6 @@ The tutorial includes setting up your development environment, tools, and gettin The first thing you need is a development environment and tools. This includes setting up your WordPress environment, Node, NPM, and your code editor. If you need help, see the [setting up your development environment documentation](/docs/getting-started/devenv/README.md). -## Quick Start - -The `@wordpress/create-block` package exists to create the necessary block scaffolding to get you started. See [create-block package documentation](https://www.npmjs.com/package/@wordpress/create-block) for additional features. This quick start assumes you have a development environment with node installed, and a WordPress site. - -From your plugins directory, to create your block run: - -```sh -npx @wordpress/create-block gutenpride --template @wordpress/create-block-tutorial-template -``` - -> Remember that you should use Node.js v14. Other versions may result in an error in the terminal. See [Node Development Tools](https://developer.wordpress.org/block-editor/getting-started/devenv/#node-development-tools) for more info. - -The [npx command](https://docs.npmjs.com/cli/v8/commands/npx) runs a command from a remote package, in this case our create-block package that will create a new directory called `gutenpride`, installs the necessary files, and builds the block plugin. If you want an interactive mode that prompts you for details, run the command without the `gutenpride` name. - -You now need to activate the plugin from inside wp-admin plugins page. - -After activation, go to the block editor and use the inserter to search and add your new block. - ## Table of Contents The create a block tutorials breaks down to the following sections. diff --git a/docs/getting-started/devenv/get-started-with-create-block.md b/docs/getting-started/devenv/get-started-with-create-block.md index 8b3a7b5867476f..3a2c6607b82cff 100644 --- a/docs/getting-started/devenv/get-started-with-create-block.md +++ b/docs/getting-started/devenv/get-started-with-create-block.md @@ -1,6 +1,6 @@ # Get started with create-block -Custom blocks for the Block Editor in WordPress are typically registered using plugins and are defined through a specific set of files. The [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package is an officially supported tool to scaffold the structure of files needed to create and register a block. It generates all the necessary code to start a project and integrates a modern JavaScript build setup (using [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts.md)) with no configuration required. +Custom blocks for the Block Editor in WordPress are typically registered using plugins and are defined through a specific set of files. The [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package is an officially supported tool to scaffold the structure of files needed to create and register a block. It generates all the necessary code to start a project and integrates a modern JavaScript build setup (using [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/)) with no configuration required. The package is designed to help developers quickly set up a block development environment following WordPress best practices. @@ -57,7 +57,7 @@ See the `wp-scripts` [package documentation](https://developer.wordpress.org/blo ### Interactive mode -For developers who prefer a more guided experience, the `create-block package` provides an interactive mode. Instead of manually specifying all options upfront, like the `slug` in the above example, this mode will prompt you for inputs step-by-step. +For developers who prefer a more guided experience, the `create-block` package provides an interactive mode. Instead of manually specifying all options upfront, like the `slug` in the above example, this mode will prompt you for inputs step-by-step. To use this mode, run the command: diff --git a/docs/getting-started/devenv/get-started-with-wp-env.md b/docs/getting-started/devenv/get-started-with-wp-env.md index 90b38d944eca6c..74942ea3ee93bf 100644 --- a/docs/getting-started/devenv/get-started-with-wp-env.md +++ b/docs/getting-started/devenv/get-started-with-wp-env.md @@ -140,4 +140,5 @@ Your environment should now be set up at http://localhost:8888. - [@wordpress/env](https://www.npmjs.com/package/@wordpress/env) (Official documentation) - [Docker Desktop](https://docs.docker.com/desktop) (Official documentation) - [Quick and easy local WordPress development with wp-env](https://developer.wordpress.org/news/2023/03/quick-and-easy-local-wordpress-development-with-wp-env/) (WordPress Developer Blog) +- [wp-env: Simple Local Environments for WordPress](https://make.wordpress.org/core/2020/03/03/wp-env-simple-local-environments-for-wordpress/) (Make WordPress Core Blog) - [`wp-env` Basics diagram](https://excalidraw.com/#json=8Tp55B-R6Z6-pNGtmenU6,_DeBR1IBxuHNIKPTVEaseA) (Excalidraw) diff --git a/docs/getting-started/quick-start-guide.md b/docs/getting-started/quick-start-guide.md new file mode 100644 index 00000000000000..4ad3998e7c27d3 --- /dev/null +++ b/docs/getting-started/quick-start-guide.md @@ -0,0 +1,44 @@ +# Quick Start Guide + +This guide is designed to demonstrate the basic principles of block development in WordPress using a hands-on approach. Following the steps below, you will create a custom block plugin that uses modern JavaScript (ESNext and JSX) in a matter of minutes. The example block displays the copyright symbol (©) and the current year, the perfect addition to any website's footer. + +## Scaffold the block plugin + +Start by ensuring you have Node.js and `npm` installed on your computer. Review the [Node.js development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/nodejs-development-environment/) guide if not. + +Next, use the [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package and the [`@wordpress/create-block-tutorial-template`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block-tutorial-template/) template to scaffold the complete “Copyright Date Block” plugin. + +
+

You can use create-block to scaffold a block just about anywhere and then use wp-env inside the generated plugin folder. This will create a local WordPress development environment with your new block plugin installed and activated.

+

If you already have your own local WordPress development environment, navigate to the plugins/ folder using the terminal.

+
+ +Choose the folder where you want to create the plugin, and then execute the following command in the terminal from within that folder: + +```sh +npx @wordpress/create-block copyright-date-block --template create-block-tutorial-template +``` + +The `slug` provided (`copyright-date-block`) defines the folder name for the scaffolded plugin and the internal block name. + +Navigate to the Plugins page of your local WordPress installation and activate the “Copyright Date Block” plugin. The example block will then be available in the Editor. + +## Basic usage + +With the plugin activated, you can explore how the block works. Use the following command to move into the newly created plugin folder and start the development process. + +```sh +cd copyright-date-block && npm start +``` + +When `create-block` scaffolds the block, it installs `wp-scripts` and adds the most common scripts to the block’s `package.json` file. Refer to the [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) article for an introduction to this package. + +The `npm start` command will start a development server and watch for changes in the block’s code, rebuilding the block whenever modifications are made. + +When you are finished making changes, run the `npm run build` command. This optimizes the block code and makes it production-ready. + +## Additional resources + +- [Get started with create-block](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) +- [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) +- [Get started with wp-env](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/) \ No newline at end of file diff --git a/docs/manifest.json b/docs/manifest.json index bc4d8bd7c3b28d..ba345e7716ee37 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -41,6 +41,12 @@ "markdown_source": "../docs/getting-started/devenv/get-started-with-wp-scripts.md", "parent": "devenv" }, + { + "title": "Quick Start Guide", + "slug": "quick-start-guide", + "markdown_source": "../docs/getting-started/quick-start-guide.md", + "parent": "getting-started" + }, { "title": "Create a Block Tutorial", "slug": "create-block", diff --git a/docs/reference-guides/block-api/block-attributes.md b/docs/reference-guides/block-api/block-attributes.md index 0fbbeeb13680e6..765d69584a6690 100644 --- a/docs/reference-guides/block-api/block-attributes.md +++ b/docs/reference-guides/block-api/block-attributes.md @@ -357,7 +357,7 @@ Attribute available in the block: ### Meta source (deprecated)
-Although attributes may be obtained from a post's meta, meta attribute sources are considered deprecated; EntityProvider and related hook APIs should be used instead, as shown in the Create Meta Block how-to. +Although attributes may be obtained from a post's meta, meta attribute sources are considered deprecated; EntityProvider and related hook APIs should be used instead, as shown in the Create Meta Block how-to.
Attributes may be obtained from a post's meta rather than from the block's representation in saved post content. For this, an attribute is required to specify its corresponding meta key under the `meta` key. diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index a888ea6caf0c0f..a58c56d7a8a94f 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -232,7 +232,7 @@ When the block declares support for `color.background`, the attributes definitio _**Note:** Deprecated since WordPress 6.3._ -This property has been replaced by [`filter.duotone`](#filter-duotone). +This property has been replaced by [`filter.duotone`](#filterduotone). ### color.gradients diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index e65853a6fbdf71..0ae5979b797047 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -771,7 +771,7 @@ Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Ju - **Name:** core/quote - **Category:** text -- **Supports:** anchor, color (background, gradients, heading, link, text), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** anchor, color (background, gradients, heading, link, text), layout (~~allowEditing~~), spacing (blockGap), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** align, citation, value ## Read More diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 7b0bd386daaf48..38a93552bcbef2 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -588,6 +588,18 @@ _Properties_ - _isDisabled_ `boolean`: Whether or not the user should be prevented from inserting this item. - _frecency_ `number`: Heuristic that combines frequency and recency. +### getLastFocus + +Returns the element of the last element that had focus when focus left the editor canvas. + +_Parameters_ + +- _state_ `Object`: Block editor state. + +_Returns_ + +- `Object`: Element. + ### getLastMultiSelectedBlockClientId Returns the client ID of the last block in the multi-selection set, or null if there is no multi-selection. @@ -1651,6 +1663,18 @@ _Parameters_ - _clientId_ `string`: The block's clientId. - _hasControlledInnerBlocks_ `boolean`: True if the block's inner blocks are controlled. +### setLastFocus + +Action that sets the element that had focus when focus leaves the editor canvas. + +_Parameters_ + +- _lastFocus_ `Object`: The last focused element. + +_Returns_ + +- `Object`: Action object. + ### setNavigationMode Action that enables or disables the navigation mode. diff --git a/docs/reference-guides/data/data-core-edit-site.md b/docs/reference-guides/data/data-core-edit-site.md index 6dea8e9b77d1b2..21cd5b2beb7b69 100644 --- a/docs/reference-guides/data/data-core-edit-site.md +++ b/docs/reference-guides/data/data-core-edit-site.md @@ -120,12 +120,11 @@ _Returns_ ### getSettings -Returns the settings, taking into account active features and permissions. +Returns the site editor settings. _Parameters_ - _state_ `Object`: Global application state. -- _setIsInserterOpen_ `Function`: Setter for the open state of the global inserter. _Returns_ @@ -222,6 +221,8 @@ _Returns_ ### addTemplate +> **Deprecated** + Action that adds a new template and sets it as the current template. _Parameters_ @@ -276,6 +277,7 @@ _Parameters_ - _postType_ `string`: The entity's post type. - _postId_ `string`: The entity's ID. +- _context_ `Object`: The entity's context. _Returns_ @@ -365,15 +367,9 @@ _Returns_ ### setPage -Resolves the template for a page and displays both. If no path is given, attempts to use the postId to generate a path like `?p=${ postId }`. - -_Parameters_ +> **Deprecated** -- _page_ `Object`: The page object. -- _page.type_ `string`: The page type. -- _page.slug_ `string`: The page slug. -- _page.path_ `string`: The page path. -- _page.context_ `Object`: The page context. +Resolves the template for a page and displays both. If no path is given, attempts to use the postId to generate a path like `?p=${ postId }`. _Returns_ @@ -383,11 +379,6 @@ _Returns_ Action that sets a template, optionally fetching it from REST API. -_Parameters_ - -- _templateId_ `number`: The template ID. -- _templateSlug_ `string`: The template slug. - _Returns_ - `Object`: Action object. diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 8a190869f99e78..ea97ce28e4d85c 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -150,6 +150,19 @@ _Returns_ - `undefined< 'edit' >`: Current user object. +### getDefaultTemplateId + +Returns the default template use to render a given query. + +_Parameters_ + +- _state_ `State`: Data state. +- _query_ `TemplateQuery`: Query. + +_Returns_ + +- `string`: The default template id for the given query. + ### getEditedEntityRecord Returns the specified entity record, merged with its edits. @@ -648,6 +661,19 @@ _Returns_ - `Object`: Action object. +### receiveDefaultTemplateId + +Returns an action object used to set the template for a given query. + +_Parameters_ + +- _query_ `Object`: The lookup query. +- _templateId_ `string`: The resolved template id. + +_Returns_ + +- `Object`: Action object. + ### receiveEntityRecords Returns an action object used in signalling that entity records have been received. diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index 8970b9202b9366..912403c4838941 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -187,7 +187,7 @@ const { createHigherOrderComponent } = wp.compose; const { InspectorControls } = wp.blockEditor; const { PanelBody } = wp.components; -const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { +const withMyPluginControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { return ( <> @@ -198,12 +198,12 @@ const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { ); }; -}, 'withInspectorControl' ); +}, 'withMyPluginControls' ); wp.hooks.addFilter( 'editor.BlockEdit', 'my-plugin/with-inspector-controls', - withInspectorControls + withMyPluginControls ); ``` @@ -212,7 +212,7 @@ wp.hooks.addFilter( ```js var el = React.createElement; -var withInspectorControls = wp.compose.createHigherOrderComponent( function ( +var withMyPluginControls = wp.compose.createHigherOrderComponent( function ( BlockEdit ) { return function ( props ) { @@ -227,12 +227,12 @@ var withInspectorControls = wp.compose.createHigherOrderComponent( function ( ) ); }; -}, 'withInspectorControls' ); +}, 'withMyPluginControls' ); wp.hooks.addFilter( 'editor.BlockEdit', 'my-plugin/with-inspector-controls', - withInspectorControls + withMyPluginControls ); ``` @@ -245,7 +245,7 @@ To mitigate this, consider whether any work you perform can be altered to run on For example, if you are adding components that only need to render when the block is _selected_, then you can use the block's "selected" state (`props.isSelected`) to conditionalize your rendering. ```js -const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { +const withMyPluginControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { return ( <> @@ -258,7 +258,7 @@ const withInspectorControls = createHigherOrderComponent( ( BlockEdit ) => { ); }; -}, 'withInspectorControl' ); +}, 'withMyPluginControls' ); ``` #### `editor.BlockListBlock` diff --git a/docs/toc.json b/docs/toc.json index cbabf3d3b737c6..8a29d2d4f10aff 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -20,6 +20,7 @@ } ] }, + { "docs/getting-started/quick-start-guide.md": [] }, { "docs/getting-started/create-block/README.md": [ { diff --git a/gutenberg.php b/gutenberg.php index 13c82f3ea6455a..3ccde2667f03aa 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.2 * Requires PHP: 7.0 - * Version: 17.0.0-rc.1 + * Version: 17.1.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/compat/wordpress-6.4/rest-api.php b/lib/compat/wordpress-6.4/rest-api.php index 7c81a6a274c03a..274eb3af945439 100644 --- a/lib/compat/wordpress-6.4/rest-api.php +++ b/lib/compat/wordpress-6.4/rest-api.php @@ -18,12 +18,3 @@ function gutenberg_register_rest_block_patterns_routes() { $block_patterns->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_rest_block_patterns_routes' ); - -/** - * Registers the Global Styles Revisions REST API routes. - */ -function gutenberg_register_global_styles_revisions_endpoints() { - $global_styles_revisions_controller = new Gutenberg_REST_Global_Styles_Revisions_Controller_6_4(); - $global_styles_revisions_controller->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' ); diff --git a/lib/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php b/lib/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php new file mode 100644 index 00000000000000..e7b2ac85f6e525 --- /dev/null +++ b/lib/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php @@ -0,0 +1,102 @@ +namespace, + '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'parent' => array( + 'description' => __( 'The ID for the parent of the global styles revision.' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the global styles revision.' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + parent::register_routes(); + } + + /** + * Retrieves one global styles revision from the collection. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $parent = $this->get_parent( $request['parent'] ); + if ( is_wp_error( $parent ) ) { + return $parent; + } + + $revision = $this->get_revision( $request['id'] ); + if ( is_wp_error( $revision ) ) { + return $revision; + } + + $response = $this->prepare_item_for_response( $revision, $request ); + return rest_ensure_response( $response ); + } + + /** + * Get the global styles revision, if the ID is valid. + * + * @since 6.5.0 + * + * @param int $id Supplied ID. + * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise. + */ + protected function get_revision( $id ) { + $error = new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid revision ID.' ), + array( 'status' => 404 ) + ); + + if ( (int) $id <= 0 ) { + return $error; + } + + $revision = get_post( (int) $id ); + if ( empty( $revision ) || empty( $revision->ID ) || 'revision' !== $revision->post_type ) { + return $error; + } + + return $revision; + } +} diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php new file mode 100644 index 00000000000000..0e9166a7c7d548 --- /dev/null +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -0,0 +1,642 @@ +. + * + * @var array + */ + private static $nav_blocks_wrapped_in_list_item = array( + 'core/navigation-link', + 'core/home-link', + 'core/site-title', + 'core/site-logo', + 'core/navigation-submenu', + ); + + /** + * Used to determine which blocks need an
  • wrapper. + * + * @var array + */ + private static $needs_list_item_wrapper = array( + 'core/site-title', + 'core/site-logo', + ); + + /** + * Keeps track of all the navigation names that have been seen. + * + * @var array + */ + private static $seen_menu_names = array(); + + /** + * Returns whether or not this is responsive navigation. + * + * @param array $attributes The block attributes. + * @return bool Returns whether or not this is responsive navigation. + */ + private static function is_responsive( $attributes ) { + /** + * This is for backwards compatibility after the `isResponsive` attribute was been removed. + */ + + $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive']; + return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute; + } + + /** + * Returns whether or not a navigation has a submenu. + * + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return bool Returns whether or not a navigation has a submenu. + */ + private static function has_submenus( $inner_blocks ) { + foreach ( $inner_blocks as $inner_block ) { + $inner_block_content = $inner_block->render(); + $p = new WP_HTML_Tag_Processor( $inner_block_content ); + if ( $p->next_tag( + array( + 'name' => 'LI', + 'class_name' => 'has-child', + ) + ) ) { + return true; + } + } + return false; + } + + /** + * Determine whether to load the view script. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return bool Returns whether or not to load the view script. + */ + private static function should_load_view_script( $attributes, $inner_blocks ) { + $has_submenus = static::has_submenus( $inner_blocks ); + $is_responsive_menu = static::is_responsive( $attributes ); + return ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; + } + + /** + * Returns whether or not a block needs a list item wrapper. + * + * @param WP_Block $block The block. + * @return bool Returns whether or not a block needs a list item wrapper. + */ + private static function does_block_need_a_list_item_wrapper( $block ) { + return in_array( $block->name, static::$needs_list_item_wrapper, true ); + } + + /** + * Returns the markup for a single inner block. + * + * @param WP_Block $inner_block The inner block. + * @return string Returns the markup for a single inner block. + */ + private static function get_markup_for_inner_block( $inner_block ) { + $inner_block_content = $inner_block->render(); + if ( ! empty( $inner_block_content ) ) { + if ( static::does_block_need_a_list_item_wrapper( $inner_block ) ) { + return '
  • ' . $inner_block_content . '
  • '; + } + + return $inner_block_content; + } + } + + /** + * Returns the html for the inner blocks of the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return string Returns the html for the inner blocks of the navigation block. + */ + private static function get_inner_blocks_html( $attributes, $inner_blocks ) { + $has_submenus = static::has_submenus( $inner_blocks ); + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + + $style = static::get_styles( $attributes ); + $class = static::get_classes( $attributes ); + $container_attributes = get_block_wrapper_attributes( + array( + 'class' => 'wp-block-navigation__container ' . $class, + 'style' => $style, + ) + ); + + $inner_blocks_html = ''; + $is_list_open = false; + + foreach ( $inner_blocks as $inner_block ) { + $is_list_item = in_array( $inner_block->name, static::$nav_blocks_wrapped_in_list_item, true ); + + if ( $is_list_item && ! $is_list_open ) { + $is_list_open = true; + $inner_blocks_html .= sprintf( + '
      ', + $container_attributes + ); + } + + if ( ! $is_list_item && $is_list_open ) { + $is_list_open = false; + $inner_blocks_html .= '
    '; + } + + $inner_blocks_html .= static::get_markup_for_inner_block( $inner_block ); + } + + if ( $is_list_open ) { + $inner_blocks_html .= ''; + } + + // Add directives to the submenu if needed. + if ( $has_submenus && $should_load_view_script ) { + $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); + $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); + } + + return $inner_blocks_html; + } + + /** + * Gets the inner blocks for the navigation block from the navigation post. + * + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks_from_navigation_post( $attributes ) { + $navigation_post = get_post( $attributes['ref'] ); + if ( ! isset( $navigation_post ) ) { + return ''; + } + + // Only published posts are valid. If this is changed then a corresponding change + // must also be implemented in `use-navigation-menu.js`. + if ( 'publish' === $navigation_post->post_status ) { + $parsed_blocks = parse_blocks( $navigation_post->post_content ); + + // 'parse_blocks' includes a null block with '\n\n' as the content when + // it encounters whitespace. This code strips it. + $compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + + // TODO - this uses the full navigation block attributes for the + // context which could be refined. + return new WP_Block_List( $compacted_blocks, $attributes ); + } + } + + /** + * Gets the inner blocks for the navigation block from the fallback. + * + * @param array $attributes The block attributes. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks_from_fallback( $attributes ) { + $fallback_blocks = block_core_navigation_get_fallback_blocks(); + + // Fallback my have been filtered so do basic test for validity. + if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { + return ''; + } + + return new WP_Block_List( $fallback_blocks, $attributes ); + } + + /** + * Gets the inner blocks for the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block $block The parsed block. + * @return WP_Block_List Returns the inner blocks for the navigation block. + */ + private static function get_inner_blocks( $attributes, $block ) { + $inner_blocks = $block->inner_blocks; + + // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. + if ( array_key_exists( 'navigationMenuId', $attributes ) ) { + $attributes['ref'] = $attributes['navigationMenuId']; + } + + // If: + // - the gutenberg plugin is active + // - `__unstableLocation` is defined + // - we have menu items at the defined location + // - we don't have a relationship to a `wp_navigation` Post (via `ref`). + // ...then create inner blocks from the classic menu assigned to that location. + if ( + defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && + array_key_exists( '__unstableLocation', $attributes ) && + ! array_key_exists( 'ref', $attributes ) && + ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) + ) { + $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); + } + + // Load inner blocks from the navigation post. + if ( array_key_exists( 'ref', $attributes ) ) { + $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes ); + } + + // If there are no inner blocks then fallback to rendering an appropriate fallback. + if ( empty( $inner_blocks ) ) { + $inner_blocks = static::get_inner_blocks_from_fallback( $attributes ); + } + + /** + * Filter navigation block $inner_blocks. + * Allows modification of a navigation block menu items. + * + * @since 6.1.0 + * + * @param \WP_Block_List $inner_blocks + */ + $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); + + $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); + if ( $post_ids ) { + _prime_post_caches( $post_ids, false, false ); + } + + return $inner_blocks; + } + + /** + * Gets the name of the current navigation, if it has one. + * + * @param array $attributes The block attributes. + * @return string Returns the name of the navigation. + */ + private static function get_navigation_name( $attributes ) { + + $navigation_name = $attributes['ariaLabel'] ?? ''; + + // Load the navigation post. + if ( array_key_exists( 'ref', $attributes ) ) { + $navigation_post = get_post( $attributes['ref'] ); + if ( ! isset( $navigation_post ) ) { + return $navigation_name; + } + + // Only published posts are valid. If this is changed then a corresponding change + // must also be implemented in `use-navigation-menu.js`. + if ( 'publish' === $navigation_post->post_status ) { + $navigation_name = $navigation_post->post_title; + + // This is used to count the number of times a navigation name has been seen, + // so that we can ensure every navigation has a unique id. + if ( isset( static::$seen_menu_names[ $navigation_name ] ) ) { + ++static::$seen_menu_names[ $navigation_name ]; + } else { + static::$seen_menu_names[ $navigation_name ] = 1; + } + } + } + + return $navigation_name; + } + + /** + * Returns the layout class for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the layout class for the navigation block. + */ + private static function get_layout_class( $attributes ) { + $layout_justification = array( + 'left' => 'items-justified-left', + 'right' => 'items-justified-right', + 'center' => 'items-justified-center', + 'space-between' => 'items-justified-space-between', + ); + + $layout_class = ''; + if ( + isset( $attributes['layout']['justifyContent'] ) && + isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) + ) { + $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; + } + if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { + $layout_class .= ' is-vertical'; + } + + if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { + $layout_class .= ' no-wrap'; + } + return $layout_class; + } + + /** + * Return classes for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the classes for the navigation block. + */ + private static function get_classes( $attributes ) { + // Restore legacy classnames for submenu positioning. + $layout_class = static::get_layout_class( $attributes ); + $colors = block_core_navigation_build_css_colors( $attributes ); + $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $is_responsive_menu = static::is_responsive( $attributes ); + + // Manually add block support text decoration as CSS class. + $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; + $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); + + $classes = array_merge( + $colors['css_classes'], + $font_sizes['css_classes'], + $is_responsive_menu ? array( 'is-responsive' ) : array(), + $layout_class ? array( $layout_class ) : array(), + $text_decoration ? array( $text_decoration_class ) : array() + ); + return implode( ' ', $classes ); + } + + /** + * Get styles for the navigation block. + * + * @param array $attributes The block attributes. + * @return string Returns the styles for the navigation block. + */ + private static function get_styles( $attributes ) { + $colors = block_core_navigation_build_css_colors( $attributes ); + $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; + return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; + } + + /** + * Get the responsive container markup + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @param string $inner_blocks_html The markup for the inner blocks. + * @return string Returns the container markup. + */ + private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + $colors = block_core_navigation_build_css_colors( $attributes ); + $modal_unique_id = wp_unique_id( 'modal-' ); + + $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; + + $responsive_container_classes = array( + 'wp-block-navigation__responsive-container', + $is_hidden_by_default ? 'hidden-by-default' : '', + implode( ' ', $colors['overlay_css_classes'] ), + ); + $open_button_classes = array( + 'wp-block-navigation__responsive-container-open', + $is_hidden_by_default ? 'always-shown' : '', + ); + + $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; + $toggle_button_icon = ''; + if ( isset( $attributes['icon'] ) ) { + if ( 'menu' === $attributes['icon'] ) { + $toggle_button_icon = ''; + } + } + $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); + $toggle_close_button_icon = ''; + $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); + $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. + $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. + + // Add Interactivity API directives to the markup if needed. + $open_button_directives = ''; + $responsive_container_directives = ''; + $responsive_dialog_directives = ''; + $close_button_directives = ''; + if ( $should_load_view_script ) { + $open_button_directives = ' + data-wp-on--click="actions.core.navigation.openMenuOnClick" + data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + '; + $responsive_container_directives = ' + data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" + data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" + data-wp-effect="effects.core.navigation.initMenu" + data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" + tabindex="-1" + '; + $responsive_dialog_directives = ' + data-wp-bind--aria-modal="selectors.core.navigation.ariaModal" + data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" + data-wp-bind--role="selectors.core.navigation.roleAttribute" + data-wp-effect="effects.core.navigation.focusFirstElement" + '; + $close_button_directives = ' + data-wp-on--click="actions.core.navigation.closeMenuOnClick" + '; + } + + return sprintf( + ' +
    +
    +
    + +
    + %2$s +
    +
    +
    +
    ', + esc_attr( $modal_unique_id ), + $inner_blocks_html, + $toggle_aria_label_open, + $toggle_aria_label_close, + esc_attr( implode( ' ', $responsive_container_classes ) ), + esc_attr( implode( ' ', $open_button_classes ) ), + esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), + $toggle_button_content, + $toggle_close_button_content, + $open_button_directives, + $responsive_container_directives, + $responsive_dialog_directives, + $close_button_directives + ); + } + + /** + * Get the wrapper attributes + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks A list of inner blocks. + * @return string Returns the navigation block markup. + */ + private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) { + $nav_menu_name = static::get_unique_navigation_name( $attributes ); + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + $is_responsive_menu = static::is_responsive( $attributes ); + $style = static::get_styles( $attributes ); + $class = static::get_classes( $attributes ); + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'class' => $class, + 'style' => $style, + 'aria-label' => $nav_menu_name, + ) + ); + + if ( $is_responsive_menu ) { + $nav_element_directives = static::get_nav_element_directives( $should_load_view_script ); + $wrapper_attributes .= ' ' . $nav_element_directives; + } + + return $wrapper_attributes; + } + + /** + * Get the nav element directives + * + * @param bool $should_load_view_script Whether or not the view script should be loaded. + * @return string the directives for the navigation element. + */ + private static function get_nav_element_directives( $should_load_view_script ) { + if ( ! $should_load_view_script ) { + return ''; + } + // When adding to this array be mindful of security concerns. + $nav_element_context = wp_json_encode( + array( + 'core' => array( + 'navigation' => array( + 'overlayOpenedBy' => array(), + 'type' => 'overlay', + 'roleAttribute' => '', + 'ariaLabel' => __( 'Menu' ), + ), + ), + ), + JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP + ); + return ' + data-wp-interactive + data-wp-context=\'' . $nav_element_context . '\' + '; + } + + /** + * Handle view script loading. + * + * @param array $attributes The block attributes. + * @param WP_Block $block The parsed block. + * @param WP_Block_List $inner_blocks The list of inner blocks. + */ + private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) { + $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); + + $view_js_file = 'wp-block-navigation-view'; + + // If the script already exists, there is no point in removing it from viewScript. + if ( ! wp_script_is( $view_js_file ) ) { + $script_handles = $block->block_type->view_script_handles; + + // If the script is not needed, and it is still in the `view_script_handles`, remove it. + if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); + } + // If the script is needed, but it was previously removed, add it again. + if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); + } + } + } + + /** + * Returns the markup for the navigation block. + * + * @param array $attributes The block attributes. + * @param WP_Block_List $inner_blocks The list of inner blocks. + * @return string Returns the navigation wrapper markup. + */ + private static function get_wrapper_markup( $attributes, $inner_blocks ) { + $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks ); + if ( static::is_responsive( $attributes ) ) { + return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ); + } + return $inner_blocks_html; + } + + /** + * Returns a unique name for the navigation. + * + * @param array $attributes The block attributes. + * @return string Returns a unique name for the navigation. + */ + private static function get_unique_navigation_name( $attributes ) { + $nav_menu_name = static::get_navigation_name( $attributes ); + + // If the menu name has been used previously then append an ID + // to the name to ensure uniqueness across a given post. + if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) { + $count = static::$seen_menu_names[ $nav_menu_name ]; + $nav_menu_name = $nav_menu_name . ' ' . ( $count ); + } + + return $nav_menu_name; + } + + /** + * Renders the navigation block. + * + * @param array $attributes The block attributes. + * @param string $content The saved content. + * @param WP_Block $block The parsed block. + * @return string Returns the navigation block markup. + */ + public static function render( $attributes, $content, $block ) { + /** + * Deprecated: + * The rgbTextColor and rgbBackgroundColor attributes + * have been deprecated in favor of + * customTextColor and customBackgroundColor ones. + * Move the values from old attrs to the new ones. + */ + if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { + $attributes['customTextColor'] = $attributes['rgbTextColor']; + } + + if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { + $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; + } + + unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); + + $inner_blocks = static::get_inner_blocks( $attributes, $block ); + // Prevent navigation blocks referencing themselves from rendering. + if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { + return ''; + } + + static::handle_view_script_loading( $attributes, $block, $inner_blocks ); + + return sprintf( + '', + static::get_nav_wrapper_attributes( $attributes, $inner_blocks ), + static::get_wrapper_markup( $attributes, $inner_blocks ) + ); + } +} diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php new file mode 100644 index 00000000000000..dd372eff7943b7 --- /dev/null +++ b/lib/compat/wordpress-6.5/rest-api.php @@ -0,0 +1,21 @@ +register_routes(); +} + +add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' ); diff --git a/lib/experimental/data-views.php b/lib/experimental/data-views.php index e0346184ffc21c..e9fb2134f3b39c 100644 --- a/lib/experimental/data-views.php +++ b/lib/experimental/data-views.php @@ -40,7 +40,7 @@ function _gutenberg_register_data_views_post_type() { 'wp_dataviews_type', array( 'wp_dataviews' ), array( - 'public' => true, + 'public' => false, 'hierarchical' => false, 'labels' => array( 'name' => __( 'Dataview types', 'gutenberg' ), diff --git a/lib/experimental/fonts-api/class-wp-fonts-resolver.php b/lib/experimental/fonts-api/class-wp-fonts-resolver.php index 144f7b30acc153..efa66839cd39ca 100644 --- a/lib/experimental/fonts-api/class-wp-fonts-resolver.php +++ b/lib/experimental/fonts-api/class-wp-fonts-resolver.php @@ -202,14 +202,24 @@ private static function get_settings() { $settings = static::set_tyopgraphy_settings_array_structure( $settings ); } + // Initialize the font families from variation if set and is an array, otherwise default to an empty array. + $variation_font_families = ( isset( $variation['settings']['typography']['fontFamilies']['theme'] ) && is_array( $variation['settings']['typography']['fontFamilies']['theme'] ) ) + ? $variation['settings']['typography']['fontFamilies']['theme'] + : array(); + // Merge the variation settings with the global settings. $settings['typography']['fontFamilies']['theme'] = array_merge( $settings['typography']['fontFamilies']['theme'], - $variation['settings']['typography']['fontFamilies']['theme'] + $variation_font_families ); // Make sure there are no duplicates. - $settings['typography']['fontFamilies'] = array_unique( $settings['typography']['fontFamilies'] ); + $settings['typography']['fontFamilies'] = array_unique( $settings['typography']['fontFamilies'], SORT_REGULAR ); + + // The font families from settings might become null after running the `array_unique`. + if ( ! isset( $settings['typography']['fontFamilies']['theme'] ) || ! is_array( $settings['typography']['fontFamilies']['theme'] ) ) { + $settings['typography']['fontFamilies']['theme'] = array(); + } } return $settings; diff --git a/lib/experimental/fonts/font-library/class-wp-font-family.php b/lib/experimental/fonts/font-library/class-wp-font-family.php index 2897811f3b70b5..a4f55d8c0cece7 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family.php @@ -394,6 +394,11 @@ private function download_or_move_font_faces( $files ) { continue; } + // If the font face requires the use of the filesystem, create the fonts dir if it doesn't exist. + if ( ! empty( $font_face['downloadFromUrl'] ) && ! empty( $font_face['uploadedFile'] ) ) { + wp_mkdir_p( WP_Font_Library::get_fonts_dir() ); + } + // If installing google fonts, download the font face assets. if ( ! empty( $font_face['downloadFromUrl'] ) ) { $new_font_face = $this->download_font_face_assets( $new_font_face ); diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php index 19dfcaab49533c..9655178d706679 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php @@ -344,6 +344,18 @@ public function update_font_library_permissions_check() { return true; } + /** + * Checks whether the font directory exists or not. + * + * @since 6.5.0 + * + * @return bool Whether the font directory exists. + */ + private function has_upload_directory() { + $upload_dir = WP_Font_Library::get_fonts_dir(); + return is_dir( $upload_dir ); + } + /** * Checks whether the user has write permissions to the temp and fonts directories. * @@ -418,12 +430,29 @@ public function install_fonts( $request ) { $response_status = 400; } - if ( $this->needs_write_permission( $fonts_to_install ) && ! $this->has_write_permission() ) { - $errors[] = new WP_Error( - 'cannot_write_fonts_folder', - __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ) - ); - $response_status = 500; + if ( $this->needs_write_permission( $fonts_to_install ) ) { + $upload_dir = WP_Font_Library::get_fonts_dir(); + if ( ! $this->has_upload_directory() ) { + if ( ! wp_mkdir_p( $upload_dir ) ) { + $errors[] = new WP_Error( + 'cannot_create_fonts_folder', + sprintf( + /* translators: %s: Directory path. */ + __( 'Error: Unable to create directory %s.', 'gutenberg' ), + esc_html( $upload_dir ) + ) + ); + $response_status = 500; + } + } + + if ( $this->has_upload_directory() && ! $this->has_write_permission() ) { + $errors[] = new WP_Error( + 'cannot_write_fonts_folder', + __( 'Error: WordPress does not have permission to write the fonts folder on your server.', 'gutenberg' ) + ); + $response_status = 500; + } } if ( ! empty( $errors ) ) { diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index 7d9ccdca453b8f..e717b2e5539431 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -27,17 +27,26 @@ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_4 { * * @var array */ - public static $root_blocks = array(); + public static $root_block = null; /** - * Add a root block to the list. + * Add a root block to the variable. * * @param array $block The block to add. * * @return void */ - public static function add_root_block( $block ) { - self::$root_blocks[] = md5( serialize( $block ) ); + public static function mark_root_block( $block ) { + self::$root_block = md5( serialize( $block ) ); + } + + /** + * Remove a root block to the variable. + * + * @return void + */ + public static function unmark_root_block() { + self::$root_block = null; } /** @@ -47,8 +56,17 @@ public static function add_root_block( $block ) { * * @return bool True if block is a root block, false otherwise. */ - public static function is_root_block( $block ) { - return in_array( md5( serialize( $block ) ), self::$root_blocks, true ); + public static function is_marked_as_root_block( $block ) { + return md5( serialize( $block ) ) === self::$root_block; + } + + /** + * Check if a root block has already been defined. + * + * @return bool True if block is a root block, false otherwise. + */ + public static function has_root_block() { + return isset( self::$root_block ); } @@ -92,6 +110,75 @@ public function next_balanced_closer() { return false; } + /** + * Traverses the HTML searching for Interactivity API directives and processing + * them. + * + * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. + * @param string $prefix Attribute prefix. + * @param string[] $directives Directives. + * + * @return WP_Directive_Processor The modified instance of the + * WP_Directive_Processor. + */ + public function process_rendered_html( $tags, $prefix, $directives ) { + $context = new WP_Directive_Context(); + $tag_stack = array(); + + while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + $tag_name = $tags->get_tag(); + + // Is this a tag that closes the latest opening tag? + if ( $tags->is_tag_closer() ) { + if ( 0 === count( $tag_stack ) ) { + continue; + } + + list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); + if ( $latest_opening_tag_name === $tag_name ) { + array_pop( $tag_stack ); + + // If the matching opening tag didn't have any directives, we move on. + if ( 0 === count( $attributes ) ) { + continue; + } + } + } else { + $attributes = array(); + foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { + /* + * Removes the part after the double hyphen before looking for + * the directive processor inside `$directives`, e.g., "wp-bind" + * from "wp-bind--src" and "wp-context" from "wp-context" etc... + */ + list( $type ) = WP_Directive_Processor::parse_attribute_name( $name ); + if ( array_key_exists( $type, $directives ) ) { + $attributes[] = $type; + } + } + + /* + * If this is an open tag, and if it either has directives, or if + * we're inside a tag that does, take note of this tag and its + * directives so we can call its directive processor once we + * encounter the matching closing tag. + */ + if ( + ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && + ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) + ) { + $tag_stack[] = array( $tag_name, $attributes ); + } + } + + foreach ( $attributes as $attribute ) { + call_user_func( $directives[ $attribute ], $tags, $context ); + } + } + + return $tags; + } + /** * Return the content between two balanced tags. * diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 41223c08158869..064fc8ea62cbb2 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -8,37 +8,9 @@ */ /** - * Process directives in each block. - * - * @param string $block_content The block content. - * @param array $block The full block. - * - * @return string Filtered block content. - */ -function gutenberg_interactivity_process_directives_in_root_blocks( $block_content, $block ) { - // Don't process inner blocks or root blocks that don't contain directives. - if ( ! WP_Directive_Processor::is_root_block( $block ) || strpos( $block_content, 'data-wp-' ) === false ) { - return $block_content; - } - - // TODO: Add some directive/components registration mechanism. - $directives = array( - 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', - 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', - 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', - ); - - $tags = new WP_Directive_Processor( $block_content ); - $tags = gutenberg_interactivity_process_directives( $tags, 'data-wp-', $directives ); - return $tags->get_updated_html(); -} -add_filter( 'render_block', 'gutenberg_interactivity_process_directives_in_root_blocks', 10, 2 ); - -/** - * Mark the inner blocks with a temporary property so we can discard them later, - * and process only the root blocks. + * Mark if the block is a root block. Checks that there is already a root block + * in order not to mark template-parts or synced patterns as root blocks, where + * the parent is null. * * @param array $parsed_block The parsed block. * @param array $source_block The source block. @@ -46,81 +18,44 @@ function gutenberg_interactivity_process_directives_in_root_blocks( $block_conte * * @return array The parsed block. */ -function gutenberg_interactivity_mark_inner_blocks( $parsed_block, $source_block, $parent_block ) { - if ( ! isset( $parent_block ) ) { - WP_Directive_Processor::add_root_block( $parsed_block ); +function gutenberg_interactivity_mark_root_blocks( $parsed_block, $source_block, $parent_block ) { + if ( ! isset( $parent_block ) && ! WP_Directive_Processor::has_root_block() ) { + WP_Directive_Processor::mark_root_block( $parsed_block ); } + return $parsed_block; } -add_filter( 'render_block_data', 'gutenberg_interactivity_mark_inner_blocks', 10, 3 ); +add_filter( 'render_block_data', 'gutenberg_interactivity_mark_root_blocks', 10, 3 ); /** - * Process directives. + * Process directives in each root block. * - * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. - * @param string $prefix Attribute prefix. - * @param string[] $directives Directives. + * @param string $block_content The block content. + * @param array $block The full block. * - * @return WP_Directive_Processor The modified instance of the - * WP_Directive_Processor. + * @return string Filtered block content. */ -function gutenberg_interactivity_process_directives( $tags, $prefix, $directives ) { - $context = new WP_Directive_Context(); - $tag_stack = array(); - - while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = $tags->get_tag(); - - // Is this a tag that closes the latest opening tag? - if ( $tags->is_tag_closer() ) { - if ( 0 === count( $tag_stack ) ) { - continue; - } - - list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); - if ( $latest_opening_tag_name === $tag_name ) { - array_pop( $tag_stack ); +function gutenberg_process_directives_in_root_blocks( $block_content, $block ) { + if ( WP_Directive_Processor::is_marked_as_root_block( $block ) ) { + WP_Directive_Processor::unmark_root_block(); + $directives = array( + 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', + 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', + 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', + 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', + 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', + ); + + $tags = new WP_Directive_Processor( $block_content ); + $tags = $tags->process_rendered_html( $tags, 'data-wp-', $directives ); + return $tags->get_updated_html(); - // If the matching opening tag didn't have any directives, we move on. - if ( 0 === count( $attributes ) ) { - continue; - } - } - } else { - $attributes = array(); - foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { - /* - * Removes the part after the double hyphen before looking for - * the directive processor inside `$directives`, e.g., "wp-bind" - * from "wp-bind--src" and "wp-context" from "wp-context" etc... - */ - list( $type ) = WP_Directive_Processor::parse_attribute_name( $name ); - if ( array_key_exists( $type, $directives ) ) { - $attributes[] = $type; - } - } - - /* - * If this is an open tag, and if it either has directives, or if - * we're inside a tag that does, take note of this tag and its - * directives so we can call its directive processor once we - * encounter the matching closing tag. - */ - if ( - ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && - ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) - ) { - $tag_stack[] = array( $tag_name, $attributes ); - } - } - - foreach ( $attributes as $attribute ) { - call_user_func( $directives[ $attribute ], $tags, $context ); - } } - return $tags; + return $block_content; } +add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 ); + /** * Resolve the reference using the store and the context from the provided path. diff --git a/lib/load.php b/lib/load.php index 5740e43b5f76a1..38111d9ed5d3d5 100644 --- a/lib/load.php +++ b/lib/load.php @@ -56,6 +56,10 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; + // WordPress 6.5 compat. + require_once __DIR__ . '/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php'; + require_once __DIR__ . '/compat/wordpress-6.5/rest-api.php'; + // Plugin specific code. require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php'; require_once __DIR__ . '/rest-api.php'; @@ -119,6 +123,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.5 compat. require __DIR__ . '/compat/wordpress-6.5/block-patterns.php'; +require __DIR__ . '/compat/wordpress-6.5/class-wp-navigation-block-renderer.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/package-lock.json b/package-lock.json index 31325d1b1343d3..158d3ab9e25c14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.0.0-rc.1", + "version": "17.1.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.0.0-rc.1", + "version": "17.1.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -84,6 +84,7 @@ "devDependencies": { "@actions/core": "1.9.1", "@actions/github": "5.0.0", + "@ariakit/test": "^0.3.0", "@babel/core": "7.16.0", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-syntax-jsx": "7.16.0", @@ -95,7 +96,7 @@ "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.6.0", - "@playwright/test": "1.32.0", + "@playwright/test": "1.39.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@storybook/addon-a11y": "7.2.2", "@storybook/addon-actions": "7.2.2", @@ -1664,6 +1665,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.0.tgz", "integrity": "sha512-fngAzkzCl9zM4O/tzVaUf7ixWUAk779hjTegr/zcegoxaMS5SmaLhNQ7RU0AGx06LrhJse6GYGy8ZtK58HP/EQ==", + "dev": true, "dependencies": { "@ariakit/core": "0.3.3", "@testing-library/dom": "^8.0.0 || ^9.0.0" @@ -1681,6 +1683,12 @@ } } }, + "node_modules/@ariakit/test/node_modules/@ariakit/core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.3.tgz", + "integrity": "sha512-8x77R0aE9O9pheygg+h/z0oU9Wx/Xdlr7nfkl4klGnkJma8/nAhJ2RrchCTQCUef4WMsRnq/doCz8m/sslP6CA==", + "dev": true + }, "node_modules/@aw-web-design/x-default-browser": { "version": "1.4.126", "resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz", @@ -7136,22 +7144,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz", - "integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", "dev": true, "dependencies": { - "@types/node": "*", - "playwright-core": "1.32.0" + "playwright": "1.39.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "fsevents": "2.3.2" + "node": ">=16" } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { @@ -15286,6 +15290,7 @@ "version": "9.3.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -15304,6 +15309,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "engines": { "node": ">=10" }, @@ -15315,6 +15321,7 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, "dependencies": { "deep-equal": "^2.0.5" } @@ -15323,6 +15330,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -15350,12 +15358,14 @@ "node_modules/@testing-library/dom/node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "node_modules/@testing-library/dom/node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -15368,7 +15378,8 @@ "node_modules/@testing-library/dom/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true }, "node_modules/@testing-library/jest-dom": { "version": "5.16.5", @@ -15571,7 +15582,8 @@ "node_modules/@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==" + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "dev": true }, "node_modules/@types/async-lock": { "version": "1.4.0", @@ -19897,6 +19909,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -20236,6 +20249,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -21674,6 +21688,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -25111,6 +25126,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -25691,7 +25707,8 @@ "node_modules/dom-accessibility-api": { "version": "0.5.14", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz", - "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==" + "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", + "dev": true }, "node_modules/dom-converter": { "version": "0.2.0", @@ -25839,11 +25856,6 @@ "node": ">=12" } }, - "node_modules/downloadjs": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", - "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==" - }, "node_modules/downshift": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.0.tgz", @@ -26314,6 +26326,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -26332,7 +26345,8 @@ "node_modules/es-get-iterator/node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "node_modules/es-module-lexer": { "version": "1.3.1", @@ -28720,6 +28734,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "dependencies": { "is-callable": "^1.1.3" } @@ -29106,6 +29121,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -29339,6 +29355,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -29829,6 +29846,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -29990,6 +30008,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -30006,6 +30025,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.1.1" }, @@ -30017,6 +30037,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -30028,6 +30049,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -30039,6 +30061,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -31212,6 +31235,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, "dependencies": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -31341,6 +31365,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -31356,6 +31381,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -31374,6 +31400,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -31397,6 +31424,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -31432,6 +31460,7 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -31494,6 +31523,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -31687,6 +31717,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -31734,6 +31765,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -31844,6 +31876,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -31868,6 +31901,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -31876,6 +31910,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -31904,6 +31939,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -31918,6 +31954,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -31944,6 +31981,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, "dependencies": { "which-typed-array": "^1.1.11" }, @@ -31975,6 +32013,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -31995,6 +32034,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -36659,6 +36699,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, "bin": { "lz-string": "bin/bin.js" } @@ -41611,6 +41652,7 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -41619,6 +41661,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -41634,6 +41677,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "engines": { "node": ">= 0.4" } @@ -41653,6 +41697,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -43229,16 +43274,34 @@ "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", "dev": true }, - "node_modules/playwright-core": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.0.tgz", - "integrity": "sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==", + "node_modules/playwright": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", "dev": true, + "dependencies": { + "playwright-core": "1.39.0" + }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright/node_modules/playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" } }, "node_modules/please-upgrade-node": { @@ -46370,6 +46433,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -48133,6 +48197,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -48969,6 +49034,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, "dependencies": { "internal-slot": "^1.0.4" }, @@ -52344,6 +52410,12 @@ "node": ">= 8" } }, + "node_modules/web-vitals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.0.tgz", + "integrity": "sha512-f5YnCHVG9Y6uLCePD4tY8bO/Ge15NPEQWtvm3tPzDKygloiqtb4SVqRHBcrIAqo2ztqX5XueqDn97zHF0LdT6w==", + "dev": true + }, "node_modules/webdriver": { "version": "8.16.20", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.16.20.tgz", @@ -53601,6 +53673,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -53616,6 +53689,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, "dependencies": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -53635,6 +53709,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -54766,7 +54841,6 @@ "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.3.5", - "@ariakit/test": "^0.3.0", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -55196,7 +55270,8 @@ "form-data": "^4.0.0", "get-port": "^5.1.1", "lighthouse": "^10.4.0", - "mime": "^3.0.0" + "mime": "^3.0.0", + "web-vitals": "^3.5.0" }, "engines": { "node": ">=12" @@ -55300,6 +55375,7 @@ "@tanstack/react-table": "^8.10.3", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/blob": "file:../blob", "@wordpress/block-editor": "file:../block-editor", "@wordpress/block-library": "file:../block-library", "@wordpress/blocks": "file:../blocks", @@ -55340,7 +55416,6 @@ "classnames": "^2.3.1", "colord": "^2.9.2", "deepmerge": "^4.3.0", - "downloadjs": "^1.4.7", "fast-deep-equal": "^3.1.3", "is-plain-object": "^5.0.0", "memize": "^2.1.0", @@ -55871,6 +55946,7 @@ "dependencies": { "@babel/runtime": "^7.16.0", "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/blob": "file:../blob", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/element": "file:../element", @@ -56141,7 +56217,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.107.0", + "version": "1.108.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -56154,7 +56230,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.107.0", + "version": "1.108.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -56165,7 +56241,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.107.0", + "version": "1.108.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56443,7 +56519,7 @@ "minimist": "^1.2.0", "npm-package-json-lint": "^6.4.0", "npm-packlist": "^3.0.0", - "playwright-core": "1.32.0", + "playwright-core": "1.39.0", "postcss": "^8.4.5", "postcss-loader": "^6.2.1", "prettier": "npm:wp-prettier@3.0.3", @@ -56470,11 +56546,23 @@ "npm": ">=6.14.4" }, "peerDependencies": { - "@playwright/test": "^1.32.0", + "@playwright/test": "^1.39.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, + "packages/scripts/node_modules/playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "packages/server-side-render": { "name": "@wordpress/server-side-render", "version": "4.22.0", @@ -57713,9 +57801,18 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.0.tgz", "integrity": "sha512-fngAzkzCl9zM4O/tzVaUf7ixWUAk779hjTegr/zcegoxaMS5SmaLhNQ7RU0AGx06LrhJse6GYGy8ZtK58HP/EQ==", + "dev": true, "requires": { "@ariakit/core": "0.3.3", "@testing-library/dom": "^8.0.0 || ^9.0.0" + }, + "dependencies": { + "@ariakit/core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.3.tgz", + "integrity": "sha512-8x77R0aE9O9pheygg+h/z0oU9Wx/Xdlr7nfkl4klGnkJma8/nAhJ2RrchCTQCUef4WMsRnq/doCz8m/sslP6CA==", + "dev": true + } } }, "@aw-web-design/x-default-browser": { @@ -61619,14 +61716,12 @@ } }, "@playwright/test": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.32.0.tgz", - "integrity": "sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==", + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz", + "integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==", "dev": true, "requires": { - "@types/node": "*", - "fsevents": "2.3.2", - "playwright-core": "1.32.0" + "playwright": "1.39.0" } }, "@pmmmwh/react-refresh-webpack-plugin": { @@ -67393,6 +67488,7 @@ "version": "9.3.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", + "dev": true, "requires": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -67407,12 +67503,14 @@ "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true }, "aria-query": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, "requires": { "deep-equal": "^2.0.5" } @@ -67421,6 +67519,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", + "dev": true, "requires": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -67445,12 +67544,14 @@ "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, "pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "requires": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -67460,7 +67561,8 @@ "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true } } }, @@ -67615,7 +67717,8 @@ "@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", - "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==" + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", + "dev": true }, "@types/async-lock": { "version": "1.4.0", @@ -69992,7 +70095,6 @@ "version": "file:packages/components", "requires": { "@ariakit/react": "^0.3.5", - "@ariakit/test": "^0.3.0", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -70293,7 +70395,8 @@ "form-data": "^4.0.0", "get-port": "^5.1.1", "lighthouse": "^10.4.0", - "mime": "^3.0.0" + "mime": "^3.0.0", + "web-vitals": "^3.5.0" } }, "@wordpress/e2e-tests": { @@ -70366,6 +70469,7 @@ "@tanstack/react-table": "^8.10.3", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/blob": "file:../blob", "@wordpress/block-editor": "file:../block-editor", "@wordpress/block-library": "file:../block-library", "@wordpress/blocks": "file:../blocks", @@ -70406,7 +70510,6 @@ "classnames": "^2.3.1", "colord": "^2.9.2", "deepmerge": "^4.3.0", - "downloadjs": "^1.4.7", "fast-deep-equal": "^3.1.3", "is-plain-object": "^5.0.0", "memize": "^2.1.0", @@ -70753,6 +70856,7 @@ "requires": { "@babel/runtime": "^7.16.0", "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/blob": "file:../blob", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/element": "file:../element", @@ -71116,7 +71220,7 @@ "minimist": "^1.2.0", "npm-package-json-lint": "^6.4.0", "npm-packlist": "^3.0.0", - "playwright-core": "1.32.0", + "playwright-core": "1.39.0", "postcss": "^8.4.5", "postcss-loader": "^6.2.1", "prettier": "npm:wp-prettier@3.0.3", @@ -71134,6 +71238,14 @@ "webpack-bundle-analyzer": "^4.9.1", "webpack-cli": "^5.1.4", "webpack-dev-server": "^4.15.1" + }, + "dependencies": { + "playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true + } } }, "@wordpress/server-side-render": { @@ -72242,6 +72354,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, "requires": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -72507,7 +72620,8 @@ "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true }, "axe-core": { "version": "4.7.2", @@ -73641,6 +73755,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -76252,6 +76367,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, "requires": { "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" @@ -76697,7 +76813,8 @@ "dom-accessibility-api": { "version": "0.5.14", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz", - "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==" + "integrity": "sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==", + "dev": true }, "dom-converter": { "version": "0.2.0", @@ -76817,11 +76934,6 @@ "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", "dev": true }, - "downloadjs": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/downloadjs/-/downloadjs-1.4.7.tgz", - "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==" - }, "downshift": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.0.tgz", @@ -77207,6 +77319,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -77222,7 +77335,8 @@ "isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true } } }, @@ -79055,6 +79169,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "requires": { "is-callable": "^1.1.3" } @@ -79354,7 +79469,8 @@ "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true }, "gauge": { "version": "4.0.4", @@ -79533,6 +79649,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -79916,6 +80033,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, "requires": { "get-intrinsic": "^1.1.3" } @@ -80045,7 +80163,8 @@ "has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==" + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true }, "has-flag": { "version": "3.0.0", @@ -80056,6 +80175,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, "requires": { "get-intrinsic": "^1.1.1" } @@ -80063,17 +80183,20 @@ "has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true }, "has-tostringtag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -80956,6 +81079,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, "requires": { "get-intrinsic": "^1.2.0", "has": "^1.0.3", @@ -81058,6 +81182,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -81067,6 +81192,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -81082,6 +81208,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "requires": { "has-bigints": "^1.0.1" } @@ -81099,6 +81226,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -81121,7 +81249,8 @@ "is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true }, "is-ci": { "version": "2.0.0", @@ -81170,6 +81299,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -81297,7 +81427,8 @@ "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==" + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true }, "is-nan": { "version": "1.3.2", @@ -81337,6 +81468,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -81406,6 +81538,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -81420,12 +81553,14 @@ "is-set": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==" + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true }, "is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "requires": { "call-bind": "^1.0.2" } @@ -81448,6 +81583,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "requires": { "has-tostringtag": "^1.0.0" } @@ -81456,6 +81592,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "requires": { "has-symbols": "^1.0.2" } @@ -81473,6 +81610,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, "requires": { "which-typed-array": "^1.1.11" } @@ -81491,7 +81629,8 @@ "is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==" + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true }, "is-weakref": { "version": "1.0.2", @@ -81506,6 +81645,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, "requires": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -85082,7 +85222,8 @@ "lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==" + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true }, "macos-release": { "version": "2.2.0", @@ -88961,12 +89102,14 @@ "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true }, "object-is": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -88975,7 +89118,8 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -88989,6 +89133,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -90185,11 +90330,23 @@ "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==", "dev": true }, - "playwright-core": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.32.0.tgz", - "integrity": "sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==", - "dev": true + "playwright": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz", + "integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.39.0" + }, + "dependencies": { + "playwright-core": { + "version": "1.39.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", + "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", + "dev": true + } + } }, "please-upgrade-node": { "version": "3.2.0", @@ -92533,6 +92690,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, "requires": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -93925,6 +94083,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -94574,6 +94733,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "dev": true, "requires": { "internal-slot": "^1.0.4" } @@ -97126,6 +97286,12 @@ "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", "dev": true }, + "web-vitals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.0.tgz", + "integrity": "sha512-f5YnCHVG9Y6uLCePD4tY8bO/Ge15NPEQWtvm3tPzDKygloiqtb4SVqRHBcrIAqo2ztqX5XueqDn97zHF0LdT6w==", + "dev": true + }, "webdriver": { "version": "8.16.20", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.16.20.tgz", @@ -98025,6 +98191,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "requires": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -98037,6 +98204,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, "requires": { "is-map": "^2.0.1", "is-set": "^2.0.1", @@ -98053,6 +98221,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", diff --git a/package.json b/package.json index 6155e483f00820..eea10b3180540b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.0.0-rc.1", + "version": "17.1.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -96,6 +96,7 @@ "devDependencies": { "@actions/core": "1.9.1", "@actions/github": "5.0.0", + "@ariakit/test": "^0.3.0", "@babel/core": "7.16.0", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-syntax-jsx": "7.16.0", @@ -107,7 +108,7 @@ "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.6.0", - "@playwright/test": "1.32.0", + "@playwright/test": "1.39.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.11", "@storybook/addon-a11y": "7.2.2", "@storybook/addon-actions": "7.2.2", diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index cbe495d3787cd9..536d07ec4da34b 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -127,6 +127,7 @@ $z-layers: ( ".block-editor-template-part__selection-modal": 1000001, ".block-editor-block-rename-modal": 1000001, ".edit-site-list__rename-modal": 1000001, + ".dataviews-action-modal": 1000001, ".edit-site-swap-template-modal": 1000001, ".edit-site-template-panel__replace-template-modal": 1000001, diff --git a/packages/blob/CHANGELOG.md b/packages/blob/CHANGELOG.md index 8d88b3cc4062a3..680d53971c018d 100644 --- a/packages/blob/CHANGELOG.md +++ b/packages/blob/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New feature + +- Add `downloadBlob` function and remove `downloadjs` dependency ([#56024](https://github.com/WordPress/gutenberg/pull/56024)). + ## 3.45.0 (2023-11-02) ## 3.44.0 (2023-10-18) diff --git a/packages/blob/README.md b/packages/blob/README.md index 0305c4784d6cbd..ff28e8879602f3 100644 --- a/packages/blob/README.md +++ b/packages/blob/README.md @@ -26,6 +26,31 @@ _Returns_ - `string`: The blob URL. +### downloadBlob + +Downloads a file, e.g., a text or readable stream, in the browser. Appropriate for downloading smaller file sizes, e.g., \< 5 MB. + +Example usage: + +```js +const fileContent = JSON.stringify( + { + title: 'My Post', + }, + null, + 2 +); +const fileName = 'file.json'; + +downloadBlob( 'file.json', fileContent, 'application/json' ); +``` + +_Parameters_ + +- _filename_ `string`: File name. +- _content_ `BlobPart`: File content (BufferSource | Blob | string). +- _contentType_ `string`: (Optional) File mime type. Default is `''`. + ### getBlobByURL Retrieve a file based on a blob URL. The file must have been created by `createBlobURL` and not removed by `revokeBlobURL`, otherwise it will return `undefined`. diff --git a/packages/blob/src/index.js b/packages/blob/src/index.js index 496869703d2dac..edc2e43729a23f 100644 --- a/packages/blob/src/index.js +++ b/packages/blob/src/index.js @@ -70,3 +70,43 @@ export function isBlobURL( url ) { } return url.indexOf( 'blob:' ) === 0; } + +/** + * Downloads a file, e.g., a text or readable stream, in the browser. + * Appropriate for downloading smaller file sizes, e.g., < 5 MB. + * + * Example usage: + * + * ```js + * const fileContent = JSON.stringify( + * { + * "title": "My Post", + * }, + * null, + * 2 + * ); + * const fileName = 'file.json'; + * + * downloadBlob( 'file.json', fileContent, 'application/json' ); + * ``` + * + * @param {string} filename File name. + * @param {BlobPart} content File content (BufferSource | Blob | string). + * @param {string} contentType (Optional) File mime type. Default is `''`. + */ +export function downloadBlob( filename, content, contentType = '' ) { + if ( ! filename || ! content ) { + return; + } + + const file = new window.Blob( [ content ], { type: contentType } ); + const url = window.URL.createObjectURL( file ); + const anchorElement = document.createElement( 'a' ); + anchorElement.href = url; + anchorElement.download = filename; + anchorElement.style.display = 'none'; + document.body.appendChild( anchorElement ); + anchorElement.click(); + document.body.removeChild( anchorElement ); + window.URL.revokeObjectURL( url ); +} diff --git a/packages/blob/src/test/index.js b/packages/blob/src/test/index.js index 4e59917522b519..47dcb5019ee25e 100644 --- a/packages/blob/src/test/index.js +++ b/packages/blob/src/test/index.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { isBlobURL, getBlobTypeByURL } from '../'; +import { isBlobURL, getBlobTypeByURL, downloadBlob } from '../'; describe( 'isBlobURL', () => { it( 'returns true if the url starts with "blob:"', () => { @@ -26,3 +26,60 @@ describe( 'getBlobTypeByURL', () => { expect( getBlobTypeByURL() ).toBe( undefined ); } ); } ); + +describe( 'downloadBlob', () => { + const originalURL = window.URL; + const createObjectURL = jest.fn().mockReturnValue( 'blob:pannacotta' ); + const revokeObjectURL = jest.fn().mockReturnValue( false ); + const mockAnchorElement = document.createElement( 'a' ); + mockAnchorElement.click = jest.fn(); + const createElementSpy = jest + .spyOn( global.document, 'createElement' ) + .mockReturnValue( mockAnchorElement ); + const mockBlob = jest.fn(); + const blobSpy = jest.spyOn( window, 'Blob' ).mockReturnValue( mockBlob ); + jest.spyOn( document.body, 'appendChild' ); + jest.spyOn( document.body, 'removeChild' ); + beforeEach( () => { + // Can't seem to spy on these static methods. They are `undefined`. + // Possibly overwritten: https://github.com/WordPress/gutenberg/blob/trunk/packages/jest-preset-default/scripts/setup-globals.js#L5 + window.URL = { + createObjectURL, + revokeObjectURL, + }; + } ); + + afterAll( () => { + window.URL = originalURL; + } ); + + it( 'requires a filename argument', () => { + downloadBlob( '', '{}', 'application/json' ); + expect( blobSpy ).not.toHaveBeenCalled(); + } ); + + it( 'requires a content argument', () => { + downloadBlob( 'text.txt', '', 'text/plain' ); + expect( blobSpy ).not.toHaveBeenCalled(); + } ); + + it( 'constructs an anchor element with attributes and removes it', () => { + downloadBlob( 'filename.json', '{}', 'application/json' ); + expect( blobSpy ).toHaveBeenCalledWith( [ '{}' ], { + type: 'application/json', + } ); + expect( createObjectURL ).toHaveBeenCalledWith( mockBlob ); + expect( createElementSpy ).toHaveBeenCalledWith( 'a' ); + expect( mockAnchorElement.download ).toBe( 'filename.json' ); + expect( mockAnchorElement.href ).toBe( 'blob:pannacotta' ); + expect( mockAnchorElement ).toHaveStyle( 'display:none' ); + expect( document.body.appendChild ).toHaveBeenCalledWith( + mockAnchorElement + ); + expect( mockAnchorElement.click ).toHaveBeenCalledTimes( 1 ); + expect( document.body.removeChild ).toHaveBeenCalledWith( + mockAnchorElement + ); + expect( revokeObjectURL ).toHaveBeenCalled(); + } ); +} ); diff --git a/packages/block-editor/src/components/block-heading-level-dropdown/index.js b/packages/block-editor/src/components/block-heading-level-dropdown/index.js index be580042bb85e1..a8296d48ad2683 100644 --- a/packages/block-editor/src/components/block-heading-level-dropdown/index.js +++ b/packages/block-editor/src/components/block-heading-level-dropdown/index.js @@ -56,7 +56,7 @@ export default function HeadingLevelDropdown( { isPressed={ isActive } /> ), - label: + title: targetLevel === 0 ? __( 'Paragraph' ) : sprintf( diff --git a/packages/block-editor/src/components/block-list-appender/index.js b/packages/block-editor/src/components/block-list-appender/index.js index 7b37b93d8be8d1..68f36f7dd25058 100644 --- a/packages/block-editor/src/components/block-list-appender/index.js +++ b/packages/block-editor/src/components/block-list-appender/index.js @@ -49,10 +49,6 @@ function useAppender( rootClientId, CustomAppender ) { getBlockEditingMode, } = select( blockEditorStore ); - if ( CustomAppender === false ) { - return false; - } - if ( ! CustomAppender ) { const selectedBlockClientId = getSelectedBlockClientId(); const isParentSelected = @@ -92,6 +88,26 @@ function BlockListAppender( { renderAppender, className, tagName: TagName = 'div', +} ) { + if ( renderAppender === false ) { + return null; + } + + return ( + + ); +} + +function BlockListAppenderInner( { + rootClientId, + renderAppender, + className, + tagName: TagName, } ) { const appender = useAppender( rootClientId, renderAppender ); const isDragOver = useSelect( diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index a9d5f15f12f81b..ab57605563e1d6 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -17,12 +17,7 @@ import { useMergeRefs, useDebounce, } from '@wordpress/compose'; -import { - createContext, - useState, - useMemo, - useCallback, -} from '@wordpress/element'; +import { createContext, useMemo, useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -40,13 +35,10 @@ import { } from '../block-edit/context'; import { useTypingObserver } from '../observe-typing'; -const elementContext = createContext(); - export const IntersectionObserver = createContext(); const pendingBlockVisibilityUpdatesPerRegistry = new WeakMap(); function Root( { className, ...settings } ) { - const [ element, setElement ] = useState(); const isLargeViewport = useViewportMatch( 'medium' ); const { isOutlineMode, isFocusMode, editorMode } = useSelect( ( select ) => { @@ -115,13 +107,9 @@ function Root( { className, ...settings } ) { settings ); return ( - - -
    - { /* Ensure element and layout styles are always at the end of the document */ } -
    - - + +
    + ); } @@ -133,8 +121,6 @@ export default function BlockList( settings ) { ); } -BlockList.__unstableElementContext = elementContext; - function Items( { placeholder, rootClientId, diff --git a/packages/block-editor/src/components/block-quick-navigation/index.js b/packages/block-editor/src/components/block-quick-navigation/index.js index de33c8a427f257..7a0e7984b83cb7 100644 --- a/packages/block-editor/src/components/block-quick-navigation/index.js +++ b/packages/block-editor/src/components/block-quick-navigation/index.js @@ -5,7 +5,9 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { Button, __experimentalVStack as VStack, - __experimentalHStack as HStack, + __experimentalTruncate as Truncate, + Flex, + FlexBlock, FlexItem, } from '@wordpress/components'; import { @@ -72,10 +74,14 @@ function BlockQuickNavigationItem( { clientId } ) { isPressed={ isSelected } onClick={ () => selectBlock( clientId ) } > - - - { name } - + + + + + + { name } + + ); } diff --git a/packages/block-editor/src/components/block-styles/index.js b/packages/block-editor/src/components/block-styles/index.js index f598b35f890f15..03a6f8939c1811 100644 --- a/packages/block-editor/src/components/block-styles/index.js +++ b/packages/block-editor/src/components/block-styles/index.js @@ -13,7 +13,6 @@ import { __experimentalTruncate as Truncate, Popover, } from '@wordpress/components'; -import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -120,12 +119,3 @@ function BlockStyles( { clientId, onSwitch = noop, onHoverClassName = noop } ) { } export default BlockStyles; - -BlockStyles.Slot = () => { - deprecated( 'BlockStyles.Slot', { - version: '6.4', - since: '6.2', - } ); - - return null; -}; diff --git a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js index f9a4b7190a6dd8..84f2d4b6a7a950 100644 --- a/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/pattern-transformations-menu.js @@ -11,9 +11,7 @@ import { MenuItem, Popover, VisuallyHidden, - __unstableComposite as Composite, - __unstableUseCompositeState as useCompositeState, - __unstableCompositeItem as CompositeItem, + privateApis as componentsPrivateApis, } from '@wordpress/components'; /** @@ -21,6 +19,13 @@ import { */ import BlockPreview from '../block-preview'; import useTransformedPatterns from './use-transformed-patterns'; +import { unlock } from '../../lock-unlock'; + +const { + CompositeV2: Composite, + CompositeItemV2: CompositeItem, + useCompositeStoreV2: useCompositeStore, +} = unlock( componentsPrivateApis ); function PatternTransformationsMenu( { blocks, @@ -73,10 +78,10 @@ function PreviewPatternsPopover( { patterns, onSelect } ) { } function BlockPatternsList( { patterns, onSelect } ) { - const composite = useCompositeState(); + const composite = useCompositeStore(); return ( ) ) } ); } -function BlockPattern( { pattern, onSelect, composite } ) { +function BlockPattern( { pattern, onSelect } ) { // TODO check pattern/preview width... const baseClassName = 'block-editor-block-switcher__preview-patterns-container'; @@ -104,14 +108,16 @@ function BlockPattern( { pattern, onSelect, composite } ) { return (
    } - className={ `${ baseClassName }-list__item` } onClick={ () => onSelect( pattern.transformedBlocks ) } > { - setIsCollapsed( false ); - }, [ selectedBlockClientId ] ); - - const isLargerThanTabletViewport = useViewportMatch( 'large', '>=' ); - const isFullscreen = - document.body.classList.contains( 'is-fullscreen-mode' ); - - /** - * The following code is a workaround to fix the width of the toolbar - * it should be removed when the toolbar will be rendered inline - * FIXME: remove this layout effect when the toolbar is no longer - * absolutely positioned - */ - useLayoutEffect( () => { - // don't do anything if not fixed toolbar - if ( ! isFixed ) { - return; - } - - const blockToolbar = document.querySelector( - '.block-editor-block-contextual-toolbar' - ); - - if ( ! blockToolbar ) { - return; - } - - if ( ! blockType ) { - blockToolbar.style.width = 'initial'; - return; - } - - if ( ! isLargerThanTabletViewport ) { - // set the width of the toolbar to auto - blockToolbar.style = {}; - return; - } - - if ( isCollapsed ) { - // set the width of the toolbar to auto - blockToolbar.style.width = 'auto'; - return; - } - - // get the width of the pinned items in the post editor or widget editor - const pinnedItems = document.querySelector( - '.edit-post-header__settings, .edit-widgets-header__actions' - ); - // get the width of the left header in the site editor - const leftHeader = document.querySelector( - '.edit-site-header-edit-mode__end' - ); - - const computedToolbarStyle = window.getComputedStyle( blockToolbar ); - const computedPinnedItemsStyle = pinnedItems - ? window.getComputedStyle( pinnedItems ) - : false; - const computedLeftHeaderStyle = leftHeader - ? window.getComputedStyle( leftHeader ) - : false; - - const marginLeft = parseFloat( computedToolbarStyle.marginLeft ); - const pinnedItemsWidth = computedPinnedItemsStyle - ? parseFloat( computedPinnedItemsStyle.width ) - : 0; - const leftHeaderWidth = computedLeftHeaderStyle - ? parseFloat( computedLeftHeaderStyle.width ) - : 0; - - // set the new witdth of the toolbar - blockToolbar.style.width = `calc(100% - ${ - leftHeaderWidth + - pinnedItemsWidth + - marginLeft + - ( pinnedItems || leftHeader ? 2 : 0 ) + // Prevents button focus border from being cut off - ( isFullscreen ? 0 : 160 ) // the width of the admin sidebar expanded - }px)`; - }, [ - isFixed, - isLargerThanTabletViewport, - isCollapsed, - isFullscreen, - blockType, - ] ); - const isToolbarEnabled = - ! blockType || + blockType && hasBlockSupport( blockType, '__experimentalToolbar', true ); const hasAnyBlockControls = useHasAnyBlockControls(); if ( @@ -179,45 +79,22 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { const classes = classnames( 'block-editor-block-contextual-toolbar', { 'has-parent': hasParents && showParentSelector, 'is-fixed': isFixed, - 'is-collapsed': isCollapsed, } ); return ( - { ! isCollapsed && } - { isFixed && isLargeViewport && blockType && ( - - { - setIsCollapsed( ( collapsed ) => ! collapsed ); - toolbarButtonRef.current.focus(); - } } - label={ - isCollapsed - ? __( 'Show block tools' ) - : __( 'Hide block tools' ) - } - /> - - ) } + ); } - -export default BlockContextualToolbar; diff --git a/packages/block-editor/src/components/block-tools/empty-block-inserter.js b/packages/block-editor/src/components/block-tools/empty-block-inserter.js new file mode 100644 index 00000000000000..1d520ed72b1c69 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/empty-block-inserter.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import Inserter from '../inserter'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; + +export default function EmptyBlockInserter( { + clientId, + __unstableContentRef, +} ) { + const { + capturingClientId, + isInsertionPointVisible, + lastClientId, + rootClientId, + } = useSelectedBlockToolProps( clientId ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + return ( + +
    + +
    +
    + ); +} diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 8e3b240838fd04..bc2729fbb15990 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -6,28 +6,48 @@ import { useViewportMatch } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; +import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; /** * Internal dependencies */ +import EmptyBlockInserter from './empty-block-inserter'; import { InsertionPointOpenRef, default as InsertionPoint, } from './insertion-point'; -import SelectedBlockPopover from './selected-block-popover'; +import SelectedBlockTools from './selected-block-tools'; import { store as blockEditorStore } from '../../store'; import BlockContextualToolbar from './block-contextual-toolbar'; import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; function selector( select ) { - const { __unstableGetEditorMode, getSettings, isTyping } = - select( blockEditorStore ); + const { + getSelectedBlockClientId, + getFirstMultiSelectedBlockClientId, + getBlock, + getSettings, + __unstableGetEditorMode, + isTyping, + } = select( blockEditorStore ); + + const clientId = + getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); + + const { name = '', attributes = {} } = getBlock( clientId ) || {}; return { - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + clientId, hasFixedToolbar: getSettings().hasFixedToolbar, + hasSelectedBlock: clientId && name, isTyping: isTyping(), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + showEmptyBlockSideInserter: + clientId && + ! isTyping() && + __unstableGetEditorMode() === 'edit' && + isUnmodifiedDefaultBlock( { name, attributes } ), }; } @@ -46,10 +66,14 @@ export default function BlockTools( { ...props } ) { const isLargeViewport = useViewportMatch( 'medium' ); - const { hasFixedToolbar, isZoomOutMode, isTyping } = useSelect( - selector, - [] - ); + const { + clientId, + hasFixedToolbar, + hasSelectedBlock, + isTyping, + isZoomOutMode, + showEmptyBlockSideInserter, + } = useSelect( selector, [] ); const isMatch = useShortcutEventMatch(); const { getSelectedBlockClientIds, getBlockRootClientId } = useSelect( blockEditorStore ); @@ -106,6 +130,15 @@ export default function BlockTools( { insertBeforeBlock( clientIds[ 0 ] ); } } else if ( isMatch( 'core/block-editor/unselect', event ) ) { + if ( event.target.closest( '[role=toolbar]' ) ) { + // This shouldn't be necessary, but we have a combination of a few things all combining to create a situation where: + // - Because the block toolbar uses createPortal to populate the block toolbar fills, we can't rely on the React event bubbling to hit the onKeyDown listener for the block toolbar + // - Since we can't use the React tree, we use the DOM tree which _should_ handle the event bubbling correctly from a `createPortal` element. + // - This bubbles via the React tree, which hits this `unselect` escape keypress before the block toolbar DOM event listener has access to it. + // An alternative would be to remove the addEventListener on the navigableToolbar and use this event to handle it directly right here. That feels hacky too though. + return; + } + const clientIds = getSelectedBlockClientIds(); if ( clientIds.length ) { event.preventDefault(); @@ -129,6 +162,12 @@ export default function BlockTools( { const blockToolbarRef = usePopoverScroll( __unstableContentRef ); const blockToolbarAfterRef = usePopoverScroll( __unstableContentRef ); + // Conditions for fixed toolbar + // 1. Not zoom out mode + // 2. It's a large viewport. If it's a smaller viewport, let the floating toolbar handle it as it already has styles attached to make it render that way. + // 3. Fixed toolbar is enabled + const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport; + return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
    @@ -138,17 +177,34 @@ export default function BlockTools( { __unstableContentRef={ __unstableContentRef } /> ) } - { ! isZoomOutMode && - ( hasFixedToolbar || ! isLargeViewport ) && ( - - ) } + { /* If there is no slot available, such as in the standalone block editor, render within the editor */ } + + { ! isLargeViewport && ( // Small viewports always get a fixed toolbar + + ) } + + { showEmptyBlockSideInserter && ( + + ) } { /* Even if the toolbar is fixed, the block popover is still needed for navigation and zoom-out mode. */ } - + { ! showEmptyBlockSideInserter && hasSelectedBlock && ( + + ) } + { /* Used for the inline rich text toolbar. */ } - + { ! isTopToolbar && ( + + ) } { children } { /* Used for inline rich text popovers. */ } { - const { - isBlockInsertionPointVisible, - getBlockInsertionPoint, - getBlockOrder, - } = select( blockEditorStore ); - - if ( ! isBlockInsertionPointVisible() ) { - return false; - } - - const insertionPoint = getBlockInsertionPoint(); - const order = getBlockOrder( insertionPoint.rootClientId ); - return order[ insertionPoint.index ] === clientId; - }, - [ clientId ] - ); - const isToolbarForced = useRef( false ); - const { shouldShowContextualToolbar, canFocusHiddenToolbar } = - useShouldContextualToolbarShow(); - - const { stopTyping } = useDispatch( blockEditorStore ); - - const showEmptyBlockSideInserter = - ! isTyping && editorMode === 'edit' && isEmptyDefaultBlock; - const shouldShowBreadcrumb = - ! hasMultiSelection && - ( editorMode === 'navigation' || editorMode === 'zoom-out' ); - - useShortcut( - 'core/block-editor/focus-toolbar', - () => { - isToolbarForced.current = true; - stopTyping( true ); - }, - { - isDisabled: ! canFocusHiddenToolbar, - } - ); - - useEffect( () => { - isToolbarForced.current = false; - } ); - - // Stores the active toolbar item index so the block toolbar can return focus - // to it when re-mounting. - const initialToolbarItemIndexRef = useRef(); - - useEffect( () => { - // Resets the index whenever the active block changes so this is not - // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - initialToolbarItemIndexRef.current = undefined; - }, [ clientId ] ); - - const popoverProps = useBlockToolbarPopoverProps( { - contentElement: __unstableContentRef?.current, - clientId, - } ); - - if ( showEmptyBlockSideInserter ) { - return ( - -
    - -
    -
    - ); - } - - if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { - return ( - - { shouldShowContextualToolbar && ( - { - initialToolbarItemIndexRef.current = index; - } } - // Resets the index whenever the active block changes so - // this is not persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - key={ clientId } - /> - ) } - { shouldShowBreadcrumb && ( - - ) } - - ); - } - - return null; -} - -function wrapperSelector( select ) { - const { - getSelectedBlockClientId, - getFirstMultiSelectedBlockClientId, - getBlockRootClientId, - getBlock, - getBlockParents, - __experimentalGetBlockListSettingsForBlocks, - } = select( blockEditorStore ); - - const clientId = - getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); - - if ( ! clientId ) { - return; - } - - const { name, attributes = {} } = getBlock( clientId ) || {}; - const blockParentsClientIds = getBlockParents( clientId ); - - // Get Block List Settings for all ancestors of the current Block clientId. - const parentBlockListSettings = __experimentalGetBlockListSettingsForBlocks( - blockParentsClientIds - ); - - // Get the clientId of the topmost parent with the capture toolbars setting. - const capturingClientId = blockParentsClientIds.find( - ( parentClientId ) => - parentBlockListSettings[ parentClientId ] - ?.__experimentalCaptureToolbars - ); - - return { - clientId, - rootClientId: getBlockRootClientId( clientId ), - name, - isEmptyDefaultBlock: - name && isUnmodifiedDefaultBlock( { name, attributes } ), - capturingClientId, - }; -} - -export default function WrappedBlockPopover( { - __unstablePopoverSlot, - __unstableContentRef, -} ) { - const selected = useSelect( wrapperSelector, [] ); - - if ( ! selected ) { - return null; - } - - const { - clientId, - rootClientId, - name, - isEmptyDefaultBlock, - capturingClientId, - } = selected; - - if ( ! name ) { - return null; - } - - return ( - - ); -} diff --git a/packages/block-editor/src/components/block-tools/selected-block-tools.js b/packages/block-editor/src/components/block-tools/selected-block-tools.js new file mode 100644 index 00000000000000..dfbd3e9ba3ca3c --- /dev/null +++ b/packages/block-editor/src/components/block-tools/selected-block-tools.js @@ -0,0 +1,127 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useRef, useEffect } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import BlockSelectionButton from './block-selection-button'; +import BlockContextualToolbar from './block-contextual-toolbar'; +import { store as blockEditorStore } from '../../store'; +import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; +import { useShouldContextualToolbarShow } from '../../utils/use-should-contextual-toolbar-show'; + +export default function SelectedBlockTools( { + clientId, + showEmptyBlockSideInserter, + __unstableContentRef, +} ) { + const { + capturingClientId, + isInsertionPointVisible, + lastClientId, + rootClientId, + } = useSelectedBlockToolProps( clientId ); + + const { shouldShowBreadcrumb } = useSelect( ( select ) => { + const { hasMultiSelection, __unstableGetEditorMode } = + select( blockEditorStore ); + + const editorMode = __unstableGetEditorMode(); + + return { + shouldShowBreadcrumb: + ! hasMultiSelection() && + ( editorMode === 'navigation' || editorMode === 'zoom-out' ), + }; + }, [] ); + + const isToolbarForced = useRef( false ); + const { shouldShowContextualToolbar, canFocusHiddenToolbar } = + useShouldContextualToolbarShow(); + + const { stopTyping } = useDispatch( blockEditorStore ); + + useShortcut( + 'core/block-editor/focus-toolbar', + () => { + isToolbarForced.current = true; + stopTyping( true ); + }, + { + isDisabled: ! canFocusHiddenToolbar, + } + ); + + useEffect( () => { + isToolbarForced.current = false; + } ); + + // Stores the active toolbar item index so the block toolbar can return focus + // to it when re-mounting. + const initialToolbarItemIndexRef = useRef(); + + useEffect( () => { + // Resets the index whenever the active block changes so this is not + // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 + initialToolbarItemIndexRef.current = undefined; + }, [ clientId ] ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + if ( showEmptyBlockSideInserter ) { + return null; + } + + if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { + return ( + + { shouldShowContextualToolbar && ( + { + initialToolbarItemIndexRef.current = index; + } } + /> + ) } + { shouldShowBreadcrumb && ( + + ) } + + ); + } + + return null; +} diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 2cb2edaf1a9c7f..07f22bb4946ea2 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -118,16 +118,6 @@ } } - // Add a scrim to the right of the collapsed button. - &.is-collapsed::after { - content: ""; - position: absolute; - left: 100%; - width: $grid-unit-60; - height: 100%; - background: linear-gradient(to right, $white, transparent); - } - @include break-medium() { &.is-fixed { & > .block-editor-block-toolbar { diff --git a/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js b/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js new file mode 100644 index 00000000000000..a2783f49bb5389 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/use-selected-block-tool-props.js @@ -0,0 +1,66 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +/** + * Returns props for the selected block tools and empty block inserter. + * + * @param {string} clientId Selected block client ID. + */ +export default function useSelectedBlockToolProps( clientId ) { + const selectedBlockProps = useSelect( + ( select ) => { + const { + getBlockRootClientId, + getBlockParents, + __experimentalGetBlockListSettingsForBlocks, + isBlockInsertionPointVisible, + getBlockInsertionPoint, + getBlockOrder, + hasMultiSelection, + getLastMultiSelectedBlockClientId, + } = select( blockEditorStore ); + + const blockParentsClientIds = getBlockParents( clientId ); + + // Get Block List Settings for all ancestors of the current Block clientId. + const parentBlockListSettings = + __experimentalGetBlockListSettingsForBlocks( + blockParentsClientIds + ); + + // Get the clientId of the topmost parent with the capture toolbars setting. + const capturingClientId = blockParentsClientIds.find( + ( parentClientId ) => + parentBlockListSettings[ parentClientId ] + ?.__experimentalCaptureToolbars + ); + + let isInsertionPointVisible = false; + if ( isBlockInsertionPointVisible() ) { + const insertionPoint = getBlockInsertionPoint(); + const order = getBlockOrder( insertionPoint.rootClientId ); + isInsertionPointVisible = + order[ insertionPoint.index ] === clientId; + } + + return { + capturingClientId, + isInsertionPointVisible, + lastClientId: hasMultiSelection() + ? getLastMultiSelectedBlockClientId() + : null, + rootClientId: getBlockRootClientId( clientId ), + }; + }, + [ clientId ] + ); + + return selectedBlockProps; +} diff --git a/packages/block-editor/src/components/editable-text/index.js b/packages/block-editor/src/components/editable-text/index.js index 21366087257efb..62bb166efa8e6b 100644 --- a/packages/block-editor/src/components/editable-text/index.js +++ b/packages/block-editor/src/components/editable-text/index.js @@ -9,14 +9,7 @@ import { forwardRef } from '@wordpress/element'; import RichText from '../rich-text'; const EditableText = forwardRef( ( props, ref ) => { - return ( - - ); + return ; } ); EditableText.Content = ( { value = '', tagName: Tag = 'div', ...props } ) => { diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 28697324aa8b83..1939f75811c8c5 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -73,12 +73,13 @@ function bubbleEvent( event, Constructor, frame ) { * @param {Document} iframeDocument Document to attach listeners to. */ function useBubbleEvents( iframeDocument ) { - return useRefEffect( ( body ) => { + return useRefEffect( () => { const { defaultView } = iframeDocument; if ( ! defaultView ) { return; } const { frameElement } = defaultView; + const html = iframeDocument.documentElement; const eventTypes = [ 'dragover', 'mousemove' ]; const handlers = {}; for ( const name of eventTypes ) { @@ -88,12 +89,12 @@ function useBubbleEvents( iframeDocument ) { const Constructor = window[ constructorName ]; bubbleEvent( event, Constructor, frameElement ); }; - body.addEventListener( name, handlers[ name ] ); + html.addEventListener( name, handlers[ name ] ); } return () => { for ( const name of eventTypes ) { - body.removeEventListener( name, handlers[ name ] ); + html.removeEventListener( name, handlers[ name ] ); } }; } ); diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/explorer.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js similarity index 85% rename from packages/block-editor/src/components/inserter/block-patterns-explorer/explorer.js rename to packages/block-editor/src/components/inserter/block-patterns-explorer/index.js index e795a70679aa14..82e8a4435b0324 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/explorer.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js @@ -8,9 +8,9 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import PatternExplorerSidebar from './sidebar'; -import PatternList from './patterns-list'; -import { usePatternsCategories } from '../block-patterns-tab'; +import PatternExplorerSidebar from './pattern-explorer-sidebar'; +import PatternList from './pattern-list'; +import { usePatternCategories } from '../block-patterns-tab/use-pattern-categories'; function PatternsExplorer( { initialCategory, rootClientId } ) { const [ searchValue, setSearchValue ] = useState( '' ); @@ -20,7 +20,7 @@ function PatternsExplorer( { initialCategory, rootClientId } ) { initialCategory?.name ); - const patternCategories = usePatternsCategories( + const patternCategories = usePatternCategories( rootClientId, patternSourceFilter ); diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/sidebar.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-explorer-sidebar.js similarity index 100% rename from packages/block-editor/src/components/inserter/block-patterns-explorer/sidebar.js rename to packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-explorer-sidebar.js diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js similarity index 97% rename from packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js rename to packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js index e34816393c8d3b..7cd2320a4fd1f0 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/patterns-list.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js @@ -17,7 +17,10 @@ import InserterListbox from '../../inserter-listbox'; import { searchItems } from '../search-items'; import BlockPatternsPaging from '../../block-patterns-paging'; import usePatternsPaging from '../hooks/use-patterns-paging'; -import { allPatternsCategory, myPatternsCategory } from '../block-patterns-tab'; +import { + allPatternsCategory, + myPatternsCategory, +} from '../block-patterns-tab/utils'; function PatternsListHeader( { filterValue, filteredBlockPatternsLength } ) { if ( ! filterValue ) { diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab.js b/packages/block-editor/src/components/inserter/block-patterns-tab.js deleted file mode 100644 index 2dba8e08fa7476..00000000000000 --- a/packages/block-editor/src/components/inserter/block-patterns-tab.js +++ /dev/null @@ -1,448 +0,0 @@ -/** - * WordPress dependencies - */ -import { - useMemo, - useState, - useCallback, - useRef, - useEffect, -} from '@wordpress/element'; -import { _x, __, _n, isRTL, sprintf } from '@wordpress/i18n'; -import { useViewportMatch } from '@wordpress/compose'; -import { - __experimentalItemGroup as ItemGroup, - __experimentalItem as Item, - __experimentalHStack as HStack, - __experimentalVStack as VStack, - __experimentalHeading as Heading, - __experimentalText as Text, - FlexBlock, - Button, -} from '@wordpress/components'; -import { Icon, chevronRight, chevronLeft } from '@wordpress/icons'; -import { focus } from '@wordpress/dom'; -import { speak } from '@wordpress/a11y'; - -/** - * Internal dependencies - */ -import usePatternsState from './hooks/use-patterns-state'; -import BlockPatternList from '../block-patterns-list'; -import PatternsExplorerModal from './block-patterns-explorer/explorer'; -import MobileTabNavigation from './mobile-tab-navigation'; -import usePatternsPaging from './hooks/use-patterns-paging'; -import { - BlockPatternsSyncFilter, - SYNC_TYPES, - PATTERN_TYPES, -} from './block-patterns-filter'; - -const noop = () => {}; - -export const allPatternsCategory = { - name: 'allPatterns', - label: __( 'All patterns' ), -}; - -export const myPatternsCategory = { - name: 'myPatterns', - label: __( 'My patterns' ), -}; - -export function isPatternFiltered( pattern, sourceFilter, syncFilter ) { - const isUserPattern = pattern.name.startsWith( 'core/block' ); - const isDirectoryPattern = - pattern.source === 'core' || - pattern.source?.startsWith( 'pattern-directory' ); - - // If theme source selected, filter out user created patterns and those from - // the core patterns directory. - if ( - sourceFilter === PATTERN_TYPES.theme && - ( isUserPattern || isDirectoryPattern ) - ) { - return true; - } - - // If the directory source is selected, filter out user created patterns - // and those bundled with the theme. - if ( - sourceFilter === PATTERN_TYPES.directory && - ( isUserPattern || ! isDirectoryPattern ) - ) { - return true; - } - - // If user source selected, filter out theme patterns. Any pattern without - // an id wasn't created by a user. - if ( sourceFilter === PATTERN_TYPES.user && ! pattern.id ) { - return true; - } - - // Filter by sync status. - if ( syncFilter === SYNC_TYPES.full && pattern.syncStatus !== '' ) { - return true; - } - - if ( - syncFilter === SYNC_TYPES.unsynced && - pattern.syncStatus !== 'unsynced' && - isUserPattern - ) { - return true; - } - - return false; -} - -export function usePatternsCategories( rootClientId, sourceFilter = 'all' ) { - const [ patterns, allCategories ] = usePatternsState( - undefined, - rootClientId - ); - - const filteredPatterns = useMemo( - () => - sourceFilter === 'all' - ? patterns - : patterns.filter( - ( pattern ) => - ! isPatternFiltered( pattern, sourceFilter ) - ), - [ sourceFilter, patterns ] - ); - - const hasRegisteredCategory = useCallback( - ( pattern ) => { - if ( ! pattern.categories || ! pattern.categories.length ) { - return false; - } - - return pattern.categories.some( ( cat ) => - allCategories.some( ( category ) => category.name === cat ) - ); - }, - [ allCategories ] - ); - - // Remove any empty categories. - const populatedCategories = useMemo( () => { - const categories = allCategories - .filter( ( category ) => - filteredPatterns.some( ( pattern ) => - pattern.categories?.includes( category.name ) - ) - ) - .sort( ( a, b ) => a.label.localeCompare( b.label ) ); - - if ( - filteredPatterns.some( - ( pattern ) => ! hasRegisteredCategory( pattern ) - ) && - ! categories.find( - ( category ) => category.name === 'uncategorized' - ) - ) { - categories.push( { - name: 'uncategorized', - label: _x( 'Uncategorized' ), - } ); - } - if ( filteredPatterns.some( ( pattern ) => pattern.id ) ) { - categories.unshift( myPatternsCategory ); - } - if ( filteredPatterns.length > 0 ) { - categories.unshift( { - name: allPatternsCategory.name, - label: allPatternsCategory.label, - } ); - } - speak( - sprintf( - /* translators: %d: number of categories . */ - _n( - '%d category button displayed.', - '%d category buttons displayed.', - categories.length - ), - categories.length - ) - ); - return categories; - }, [ allCategories, filteredPatterns, hasRegisteredCategory ] ); - - return populatedCategories; -} - -export function BlockPatternsCategoryDialog( { - rootClientId, - onInsert, - onHover, - category, - showTitlesAsTooltip, - patternFilter, -} ) { - const container = useRef(); - - useEffect( () => { - const timeout = setTimeout( () => { - const [ firstTabbable ] = focus.tabbable.find( container.current ); - firstTabbable?.focus(); - } ); - return () => clearTimeout( timeout ); - }, [ category ] ); - - return ( -
    - -
    - ); -} - -export function BlockPatternsCategoryPanel( { - rootClientId, - onInsert, - onHover = noop, - category, - showTitlesAsTooltip, -} ) { - const [ allPatterns, , onClickPattern ] = usePatternsState( - onInsert, - rootClientId - ); - const [ patternSyncFilter, setPatternSyncFilter ] = useState( 'all' ); - const [ patternSourceFilter, setPatternSourceFilter ] = useState( 'all' ); - - const availableCategories = usePatternsCategories( - rootClientId, - patternSourceFilter - ); - const scrollContainerRef = useRef(); - const currentCategoryPatterns = useMemo( - () => - allPatterns.filter( ( pattern ) => { - if ( - isPatternFiltered( - pattern, - patternSourceFilter, - patternSyncFilter - ) - ) { - return false; - } - - if ( category.name === allPatternsCategory.name ) { - return true; - } - if ( category.name === myPatternsCategory.name && pattern.id ) { - return true; - } - if ( category.name !== 'uncategorized' ) { - return pattern.categories?.includes( category.name ); - } - - // The uncategorized category should show all the patterns without any category - // or with no available category. - const availablePatternCategories = - pattern.categories?.filter( ( cat ) => - availableCategories.find( - ( availableCategory ) => - availableCategory.name === cat - ) - ) ?? []; - - return availablePatternCategories.length === 0; - } ), - [ - allPatterns, - availableCategories, - category.name, - patternSourceFilter, - patternSyncFilter, - ] - ); - - const pagingProps = usePatternsPaging( - currentCategoryPatterns, - category, - scrollContainerRef - ); - const { changePage } = pagingProps; - - // Hide block pattern preview on unmount. - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect( () => () => onHover( null ), [] ); - - const onSetPatternSyncFilter = useCallback( - ( value ) => { - setPatternSyncFilter( value ); - changePage( 1 ); - }, - [ setPatternSyncFilter, changePage ] - ); - const onSetPatternSourceFilter = useCallback( - ( value ) => { - setPatternSourceFilter( value ); - changePage( 1 ); - }, - [ setPatternSourceFilter, changePage ] - ); - - return ( -
    - - - - - { category.label } - - - - - { ! currentCategoryPatterns.length && ( - - { __( 'No results found' ) } - - ) } - - - { currentCategoryPatterns.length > 0 && ( - - ) } -
    - ); -} - -function BlockPatternsTabs( { - onSelectCategory, - selectedCategory, - onInsert, - rootClientId, -} ) { - const [ showPatternsExplorer, setShowPatternsExplorer ] = useState( false ); - - const categories = usePatternsCategories( rootClientId ); - - const initialCategory = selectedCategory || categories[ 0 ]; - const isMobile = useViewportMatch( 'medium', '<' ); - return ( - <> - { ! isMobile && ( -
    - -
    - ) } - { isMobile && ( - - { ( category ) => ( - - ) } - - ) } - { showPatternsExplorer && ( - setShowPatternsExplorer( false ) } - rootClientId={ rootClientId } - /> - ) } - - ); -} - -export default BlockPatternsTabs; diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/index.js b/packages/block-editor/src/components/inserter/block-patterns-tab/index.js new file mode 100644 index 00000000000000..373b96cf569b0c --- /dev/null +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/index.js @@ -0,0 +1,118 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { __, isRTL } from '@wordpress/i18n'; +import { useViewportMatch } from '@wordpress/compose'; +import { + __experimentalItemGroup as ItemGroup, + __experimentalItem as Item, + __experimentalHStack as HStack, + FlexBlock, + Button, +} from '@wordpress/components'; +import { Icon, chevronRight, chevronLeft } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import PatternsExplorerModal from '../block-patterns-explorer'; +import MobileTabNavigation from '../mobile-tab-navigation'; +import { PatternCategoryPreviews } from './pattern-category-previews'; +import { usePatternCategories } from './use-pattern-categories'; + +function BlockPatternsTab( { + onSelectCategory, + selectedCategory, + onInsert, + rootClientId, +} ) { + const [ showPatternsExplorer, setShowPatternsExplorer ] = useState( false ); + + const categories = usePatternCategories( rootClientId ); + + const initialCategory = selectedCategory || categories[ 0 ]; + const isMobile = useViewportMatch( 'medium', '<' ); + return ( + <> + { ! isMobile && ( +
    + +
    + ) } + { isMobile && ( + + { ( category ) => ( + + ) } + + ) } + { showPatternsExplorer && ( + setShowPatternsExplorer( false ) } + rootClientId={ rootClientId } + /> + ) } + + ); +} + +export default BlockPatternsTab; diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js new file mode 100644 index 00000000000000..f548a4632eb357 --- /dev/null +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { useRef, useEffect } from '@wordpress/element'; + +import { focus } from '@wordpress/dom'; + +/** + * Internal dependencies + */ + +import { PatternCategoryPreviews } from './pattern-category-previews'; + +export function PatternCategoryPreviewPanel( { + rootClientId, + onInsert, + onHover, + category, + showTitlesAsTooltip, + patternFilter, +} ) { + const container = useRef(); + + useEffect( () => { + const timeout = setTimeout( () => { + const [ firstTabbable ] = focus.tabbable.find( container.current ); + firstTabbable?.focus(); + } ); + return () => clearTimeout( timeout ); + }, [ category ] ); + + return ( +
    + +
    + ); +} diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js new file mode 100644 index 00000000000000..2fef53cfa2a193 --- /dev/null +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js @@ -0,0 +1,175 @@ +/** + * WordPress dependencies + */ +import { + useMemo, + useState, + useCallback, + useRef, + useEffect, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalHeading as Heading, + __experimentalText as Text, + FlexBlock, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import usePatternsState from '../hooks/use-patterns-state'; +import BlockPatternList from '../../block-patterns-list'; +import usePatternsPaging from '../hooks/use-patterns-paging'; +import { PatternsFilter } from './patterns-filter'; +import { usePatternCategories } from './use-pattern-categories'; +import { + isPatternFiltered, + allPatternsCategory, + myPatternsCategory, +} from './utils'; + +const noop = () => {}; + +export function PatternCategoryPreviews( { + rootClientId, + onInsert, + onHover = noop, + category, + showTitlesAsTooltip, +} ) { + const [ allPatterns, , onClickPattern ] = usePatternsState( + onInsert, + rootClientId + ); + const [ patternSyncFilter, setPatternSyncFilter ] = useState( 'all' ); + const [ patternSourceFilter, setPatternSourceFilter ] = useState( 'all' ); + + const availableCategories = usePatternCategories( + rootClientId, + patternSourceFilter + ); + const scrollContainerRef = useRef(); + const currentCategoryPatterns = useMemo( + () => + allPatterns.filter( ( pattern ) => { + if ( + isPatternFiltered( + pattern, + patternSourceFilter, + patternSyncFilter + ) + ) { + return false; + } + + if ( category.name === allPatternsCategory.name ) { + return true; + } + if ( category.name === myPatternsCategory.name && pattern.id ) { + return true; + } + if ( category.name !== 'uncategorized' ) { + return pattern.categories?.includes( category.name ); + } + + // The uncategorized category should show all the patterns without any category + // or with no available category. + const availablePatternCategories = + pattern.categories?.filter( ( cat ) => + availableCategories.find( + ( availableCategory ) => + availableCategory.name === cat + ) + ) ?? []; + + return availablePatternCategories.length === 0; + } ), + [ + allPatterns, + availableCategories, + category.name, + patternSourceFilter, + patternSyncFilter, + ] + ); + + const pagingProps = usePatternsPaging( + currentCategoryPatterns, + category, + scrollContainerRef + ); + const { changePage } = pagingProps; + + // Hide block pattern preview on unmount. + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect( () => () => onHover( null ), [] ); + + const onSetPatternSyncFilter = useCallback( + ( value ) => { + setPatternSyncFilter( value ); + changePage( 1 ); + }, + [ setPatternSyncFilter, changePage ] + ); + const onSetPatternSourceFilter = useCallback( + ( value ) => { + setPatternSourceFilter( value ); + changePage( 1 ); + }, + [ setPatternSourceFilter, changePage ] + ); + + return ( +
    + + + + + { category.label } + + + + + { ! currentCategoryPatterns.length && ( + + { __( 'No results found' ) } + + ) } + + + { currentCategoryPatterns.length > 0 && ( + + ) } +
    + ); +} diff --git a/packages/block-editor/src/components/inserter/block-patterns-filter.js b/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js similarity index 90% rename from packages/block-editor/src/components/inserter/block-patterns-filter.js rename to packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js index 0d6cabe6c239f3..04a0f27d162dd4 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-filter.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js @@ -9,29 +9,14 @@ import { MenuItemsChoice, ExternalLink, } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; import { useMemo, createInterpolateElement } from '@wordpress/element'; /** * Internal dependencies */ -import { myPatternsCategory } from './block-patterns-tab'; - -export const PATTERN_TYPES = { - all: 'all', - synced: 'synced', - unsynced: 'unsynced', - user: 'user', - theme: 'theme', - directory: 'directory', -}; - -export const SYNC_TYPES = { - all: 'all', - full: 'fully', - unsynced: 'unsynced', -}; +import { myPatternsCategory, SYNC_TYPES, PATTERN_TYPES } from './utils'; const getShouldDisableSyncFilter = ( sourceFilter ) => sourceFilter !== PATTERN_TYPES.all && sourceFilter !== PATTERN_TYPES.user; @@ -40,7 +25,7 @@ const getShouldDisableNonUserSources = ( category ) => { return category.name === myPatternsCategory.name; }; -export function BlockPatternsSyncFilter( { +export function PatternsFilter( { setPatternSyncFilter, setPatternSourceFilter, patternSyncFilter, @@ -70,15 +55,24 @@ export function BlockPatternsSyncFilter( { const patternSyncMenuOptions = useMemo( () => [ - { value: SYNC_TYPES.all, label: __( 'All' ) }, + { + value: SYNC_TYPES.all, + label: _x( 'All', 'Option that shows all patterns' ), + }, { value: SYNC_TYPES.full, - label: __( 'Synced' ), + label: _x( + 'Synced', + 'Option that shows all synchronized patterns' + ), disabled: shouldDisableSyncFilter, }, { value: SYNC_TYPES.unsynced, - label: __( 'Not synced' ), + label: _x( + 'Not synced', + 'Option that shows all patterns that are not synchronized' + ), disabled: shouldDisableSyncFilter, }, ], diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js b/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js new file mode 100644 index 00000000000000..9c7a7a32a60c07 --- /dev/null +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js @@ -0,0 +1,96 @@ +/** + * WordPress dependencies + */ +import { useMemo, useCallback } from '@wordpress/element'; +import { _x, _n, sprintf } from '@wordpress/i18n'; + +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import usePatternsState from '../hooks/use-patterns-state'; +import { + isPatternFiltered, + allPatternsCategory, + myPatternsCategory, +} from './utils'; + +export function usePatternCategories( rootClientId, sourceFilter = 'all' ) { + const [ patterns, allCategories ] = usePatternsState( + undefined, + rootClientId + ); + + const filteredPatterns = useMemo( + () => + sourceFilter === 'all' + ? patterns + : patterns.filter( + ( pattern ) => + ! isPatternFiltered( pattern, sourceFilter ) + ), + [ sourceFilter, patterns ] + ); + + const hasRegisteredCategory = useCallback( + ( pattern ) => { + if ( ! pattern.categories || ! pattern.categories.length ) { + return false; + } + + return pattern.categories.some( ( cat ) => + allCategories.some( ( category ) => category.name === cat ) + ); + }, + [ allCategories ] + ); + + // Remove any empty categories. + const populatedCategories = useMemo( () => { + const categories = allCategories + .filter( ( category ) => + filteredPatterns.some( ( pattern ) => + pattern.categories?.includes( category.name ) + ) + ) + .sort( ( a, b ) => a.label.localeCompare( b.label ) ); + + if ( + filteredPatterns.some( + ( pattern ) => ! hasRegisteredCategory( pattern ) + ) && + ! categories.find( + ( category ) => category.name === 'uncategorized' + ) + ) { + categories.push( { + name: 'uncategorized', + label: _x( 'Uncategorized' ), + } ); + } + if ( filteredPatterns.some( ( pattern ) => pattern.id ) ) { + categories.unshift( myPatternsCategory ); + } + if ( filteredPatterns.length > 0 ) { + categories.unshift( { + name: allPatternsCategory.name, + label: allPatternsCategory.label, + } ); + } + speak( + sprintf( + /* translators: %d: number of categories . */ + _n( + '%d category button displayed.', + '%d category buttons displayed.', + categories.length + ), + categories.length + ) + ); + return categories; + }, [ allCategories, filteredPatterns, hasRegisteredCategory ] ); + + return populatedCategories; +} diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js b/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js new file mode 100644 index 00000000000000..9f222c6a2f93cd --- /dev/null +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js @@ -0,0 +1,76 @@ +/** + * WordPress dependencies + */ + +import { __ } from '@wordpress/i18n'; + +export const PATTERN_TYPES = { + all: 'all', + synced: 'synced', + unsynced: 'unsynced', + user: 'user', + theme: 'theme', + directory: 'directory', +}; + +export const SYNC_TYPES = { + all: 'all', + full: 'fully', + unsynced: 'unsynced', +}; + +export const allPatternsCategory = { + name: 'allPatterns', + label: __( 'All patterns' ), +}; + +export const myPatternsCategory = { + name: 'myPatterns', + label: __( 'My patterns' ), +}; + +export function isPatternFiltered( pattern, sourceFilter, syncFilter ) { + const isUserPattern = pattern.name.startsWith( 'core/block' ); + const isDirectoryPattern = + pattern.source === 'core' || + pattern.source?.startsWith( 'pattern-directory' ); + + // If theme source selected, filter out user created patterns and those from + // the core patterns directory. + if ( + sourceFilter === PATTERN_TYPES.theme && + ( isUserPattern || isDirectoryPattern ) + ) { + return true; + } + + // If the directory source is selected, filter out user created patterns + // and those bundled with the theme. + if ( + sourceFilter === PATTERN_TYPES.directory && + ( isUserPattern || ! isDirectoryPattern ) + ) { + return true; + } + + // If user source selected, filter out theme patterns. Any pattern without + // an id wasn't created by a user. + if ( sourceFilter === PATTERN_TYPES.user && ! pattern.id ) { + return true; + } + + // Filter by sync status. + if ( syncFilter === SYNC_TYPES.full && pattern.syncStatus !== '' ) { + return true; + } + + if ( + syncFilter === SYNC_TYPES.unsynced && + pattern.syncStatus !== 'unsynced' && + isUserPattern + ) { + return true; + } + + return false; +} diff --git a/packages/block-editor/src/components/inserter/media-tab/media-list.js b/packages/block-editor/src/components/inserter/media-tab/media-list.js index b745a54e25e9c0..bfc858bc8c4de7 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-list.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-list.js @@ -1,16 +1,17 @@ /** * WordPress dependencies */ -import { - __unstableComposite as Composite, - __unstableUseCompositeState as useCompositeState, -} from '@wordpress/components'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import { MediaPreview } from './media-preview'; +import { unlock } from '../../../lock-unlock'; + +const { CompositeV2: Composite, useCompositeStoreV2: useCompositeStore } = + unlock( componentsPrivateApis ); function MediaList( { mediaList, @@ -18,10 +19,10 @@ function MediaList( { onClick, label = __( 'Media List' ), } ) { - const composite = useCompositeState(); + const compositeStore = useCompositeStore(); return ( ) ) } diff --git a/packages/block-editor/src/components/inserter/media-tab/media-preview.js b/packages/block-editor/src/components/inserter/media-tab/media-preview.js index 88648bf96531b6..9efed229f0adf2 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -7,7 +7,6 @@ import classnames from 'classnames'; * WordPress dependencies */ import { - __unstableCompositeItem as CompositeItem, Tooltip, DropdownMenu, MenuGroup, @@ -17,6 +16,7 @@ import { Flex, FlexItem, Button, + privateApis as componentsPrivateApis, __experimentalVStack as VStack, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; @@ -33,6 +33,7 @@ import { isBlobURL } from '@wordpress/blob'; import InserterDraggableBlocks from '../../inserter-draggable-blocks'; import { getBlockAndPreviewFromMedia } from './utils'; import { store as blockEditorStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; const MAXIMUM_TITLE_LENGTH = 25; @@ -42,6 +43,8 @@ const MEDIA_OPTIONS_POPOVER_PROPS = { 'block-editor-inserter__media-list__item-preview-options__popover', }; +const { CompositeItemV2: CompositeItem } = unlock( componentsPrivateApis ); + function MediaPreviewOptions( { category, media } ) { if ( ! category.getReportUrl ) { return null; @@ -113,7 +116,7 @@ function InsertExternalImageModal( { onClose, onSubmit } ) { ); } -export function MediaPreview( { media, onClick, composite, category } ) { +export function MediaPreview( { media, onClick, category } ) { const [ showExternalUploadModal, setShowExternalUploadModal ] = useState( false ); const [ isHovered, setIsHovered ] = useState( false ); @@ -216,20 +219,22 @@ export function MediaPreview( { media, onClick, composite, category } ) { onDragStart={ onDragStart } onDragEnd={ onDragEnd } > - - { /* Adding `is-hovered` class to the wrapper element is needed - because the options Popover is rendered outside of this node. */ } -
    + { /* Adding `is-hovered` class to the wrapper element is needed + because the options Popover is rendered outside of this node. */ } +
    + + } onClick={ () => onMediaInsert( block ) } - aria-label={ title } >
    { preview } @@ -240,14 +245,14 @@ export function MediaPreview( { media, onClick, composite, category } ) { ) }
    - { ! isInserting && ( - - ) } -
    - + + { ! isInserting && ( + + ) } +
    ) } diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index ddd3d5b36b1022..71a5ba5e68adf7 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -24,9 +24,8 @@ import { useSelect } from '@wordpress/data'; import Tips from './tips'; import InserterPreviewPanel from './preview-panel'; import BlockTypesTab from './block-types-tab'; -import BlockPatternsTabs, { - BlockPatternsCategoryDialog, -} from './block-patterns-tab'; +import BlockPatternsTab from './block-patterns-tab'; +import { PatternCategoryPreviewPanel } from './block-patterns-tab/pattern-category-preview-panel'; import { MediaTab, MediaCategoryDialog, useMediaCategories } from './media-tab'; import InserterSearchResults from './search-results'; import useDebouncedInput from './hooks/use-debounced-input'; @@ -160,7 +159,7 @@ function InserterMenu( const patternsTab = useMemo( () => ( - ) } { showPatternPanel && ( - -
    +
    + ``` diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index c25ed5cd1187a8..a208fa20d242ff 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -6,7 +6,13 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Button, Spinner, Notice, TextControl } from '@wordpress/components'; +import { + Button, + Spinner, + Notice, + TextControl, + __experimentalHStack as HStack, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useRef, useState, useEffect } from '@wordpress/element'; import { focus } from '@wordpress/dom'; @@ -467,7 +473,13 @@ function LinkControl( { ) } { showActions && ( -
    + + - -
    + ) } { renderControlBottom && renderControlBottom() } diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss index 8883af42ee2ca6..7b6bbff0700a37 100644 --- a/packages/block-editor/src/components/link-control/style.scss +++ b/packages/block-editor/src/components/link-control/style.scss @@ -61,6 +61,10 @@ $preview-image-height: 140px; .block-editor-link-control__field { margin: $grid-unit-20; // allow margin collapse for vertical spacing. + .components-base-control__label { + color: $gray-900; + } + input[type="text"], // Specificity overide of URLInput defaults. &.block-editor-url-input input[type="text"].block-editor-url-input__input { @@ -92,12 +96,7 @@ $preview-image-height: 140px; } .block-editor-link-control__search-actions { - display: flex; - flex-direction: row-reverse; // put "Cancel" on the left but retain DOM order. - justify-content: flex-start; - gap: $grid-unit-10; padding: $grid-unit-10 $grid-unit-20 $grid-unit-20; - order: 20; } .block-editor-link-control__search-results-wrapper { @@ -419,6 +418,10 @@ $preview-image-height: 140px; .components-base-control__field { display: flex; // don't allow label to wrap under checkbox. + + .components-checkbox-control__label { + color: $gray-900; + } } // Cancel left margin inherited from WP Admin Forms CSS. diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index cec5d4699c7a2f..25de5483f5192e 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -19,6 +19,7 @@ import { SPACE, ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; import { useSelect, useDispatch } from '@wordpress/data'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { __, sprintf } from '@wordpress/i18n'; +import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies @@ -30,6 +31,7 @@ import ListViewExpander from './expander'; import { useBlockLock } from '../block-lock'; import { store as blockEditorStore } from '../../store'; import useListViewImages from './use-list-view-images'; +import { useListViewContext } from './context'; function ListViewBlockSelectButton( { @@ -64,10 +66,12 @@ function ListViewBlockSelectButton( getBlocksByClientId, canRemoveBlocks, } = useSelect( blockEditorStore ); - const { duplicateBlocks, removeBlocks } = useDispatch( blockEditorStore ); + const { duplicateBlocks, multiSelect, removeBlocks } = + useDispatch( blockEditorStore ); const isMatch = useShortcutEventMatch(); const isSticky = blockInformation?.positionType === 'sticky'; const images = useListViewImages( { clientId, isExpanded } ); + const { rootClientId } = useListViewContext(); const positionLabel = blockInformation?.positionLabel ? sprintf( @@ -183,6 +187,45 @@ function ListViewBlockSelectButton( updateFocusAndSelection( updatedBlocks[ 0 ], false ); } } + } else if ( isMatch( 'core/block-editor/select-all', event ) ) { + if ( event.defaultPrevented ) { + return; + } + event.preventDefault(); + + const { firstBlockRootClientId, selectedBlockClientIds } = + getBlocksToUpdate(); + const blockClientIds = getBlockOrder( firstBlockRootClientId ); + if ( ! blockClientIds.length ) { + return; + } + + // If we have selected all sibling nested blocks, try selecting up a level. + // This is a similar implementation to that used by `useSelectAll`. + // `isShallowEqual` is used for the list view instead of a length check, + // as the array of siblings of the currently focused block may be a different + // set of blocks from the current block selection if the user is focused + // on a different part of the list view from the block selection. + if ( isShallowEqual( selectedBlockClientIds, blockClientIds ) ) { + // Only select up a level if the first block is not the root block. + // This ensures that the block selection can't break out of the root block + // used by the list view, if the list view is only showing a partial hierarchy. + if ( + firstBlockRootClientId && + firstBlockRootClientId !== rootClientId + ) { + updateFocusAndSelection( firstBlockRootClientId, true ); + return; + } + } + + // Select all while passing `null` to skip focusing to the editor canvas, + // and retain focus within the list view. + multiSelect( + blockClientIds[ 0 ], + blockClientIds[ blockClientIds.length - 1 ], + null + ); } } diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 375f39a7cc3c81..4957f79fa0d481 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -69,17 +69,17 @@ function ListViewBlock( { const blockTitle = blockInformation?.name || blockInformation?.title || __( 'Untitled' ); - const block = useSelect( - ( select ) => select( blockEditorStore ).getBlock( clientId ), - [ clientId ] - ); - const blockName = useSelect( - ( select ) => select( blockEditorStore ).getBlockName( clientId ), - [ clientId ] - ); - const blockEditingMode = useSelect( - ( select ) => - select( blockEditorStore ).getBlockEditingMode( clientId ), + const { block, blockName, blockEditingMode } = useSelect( + ( select ) => { + const { getBlock, getBlockName, getBlockEditingMode } = + select( blockEditorStore ); + + return { + block: getBlock( clientId ), + blockName: getBlockName( clientId ), + blockEditingMode: getBlockEditingMode( clientId ), + }; + }, [ clientId ] ); diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 1d21c28643a73c..315a6153839d16 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -222,6 +222,7 @@ function ListViewComponent( insertedBlock, setInsertedBlock, treeGridElementRef: elementRef, + rootClientId, } ), [ draggedClientIds, @@ -233,6 +234,7 @@ function ListViewComponent( AdditionalBlockContent, insertedBlock, setInsertedBlock, + rootClientId, ] ); 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 dd3b0563c3ca8a..61df542cf58404 100644 --- a/packages/block-editor/src/components/media-replace-flow/style.scss +++ b/packages/block-editor/src/components/media-replace-flow/style.scss @@ -17,6 +17,7 @@ &.has-siblings { border-top: $border-width solid $gray-900; margin-top: $grid-unit-10; + padding-bottom: $grid-unit-10; } .block-editor-media-replace-flow__image-url-label { @@ -55,8 +56,7 @@ } .block-editor-link-control__search-actions { - top: 0; // cancel default top positioning - right: 4px; + padding: $grid-unit-10 0 0; } } } diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index 3e531c93c11989..fe216e1058f6f0 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -9,9 +9,16 @@ import { useEffect, useCallback, } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { focus } from '@wordpress/dom'; import { useShortcut } from '@wordpress/keyboard-shortcuts'; +import { ESCAPE } from '@wordpress/keycodes'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; function hasOnlyToolbarItem( elements ) { const dataProp = 'toolbarItem'; @@ -28,6 +35,7 @@ function hasFocusWithin( container ) { function focusFirstTabbableIn( container ) { const [ firstTabbable ] = focus.tabbable.find( container ); + if ( firstTabbable ) { firstTabbable.focus( { // When focusing newly mounted toolbars, @@ -38,7 +46,7 @@ function focusFirstTabbableIn( container ) { } } -function useIsAccessibleToolbar( ref ) { +function useIsAccessibleToolbar( toolbarRef ) { /* * By default, we'll assume the starting accessible state of the Toolbar * is true, as it seems to be the most common case. @@ -62,7 +70,7 @@ function useIsAccessibleToolbar( ref ) { ); const determineIsAccessibleToolbar = useCallback( () => { - const tabbables = focus.tabbable.find( ref.current ); + const tabbables = focus.tabbable.find( toolbarRef.current ); const onlyToolbarItem = hasOnlyToolbarItem( tabbables ); if ( ! onlyToolbarItem ) { deprecated( 'Using custom components as toolbar controls', { @@ -73,7 +81,7 @@ function useIsAccessibleToolbar( ref ) { } ); } setIsAccessibleToolbar( onlyToolbarItem ); - }, [] ); + }, [ toolbarRef ] ); useLayoutEffect( () => { // Toolbar buttons may be rendered asynchronously, so we use @@ -81,28 +89,32 @@ function useIsAccessibleToolbar( ref ) { const observer = new window.MutationObserver( determineIsAccessibleToolbar ); - observer.observe( ref.current, { childList: true, subtree: true } ); + observer.observe( toolbarRef.current, { + childList: true, + subtree: true, + } ); return () => observer.disconnect(); - }, [ isAccessibleToolbar ] ); + }, [ determineIsAccessibleToolbar, isAccessibleToolbar, toolbarRef ] ); return isAccessibleToolbar; } -function useToolbarFocus( - ref, +function useToolbarFocus( { + toolbarRef, focusOnMount, isAccessibleToolbar, defaultIndex, onIndexChange, - shouldUseKeyboardFocusShortcut -) { + shouldUseKeyboardFocusShortcut, + focusEditorOnEscape, +} ) { // Make sure we don't use modified versions of this prop. const [ initialFocusOnMount ] = useState( focusOnMount ); const [ initialIndex ] = useState( defaultIndex ); const focusToolbar = useCallback( () => { - focusFirstTabbableIn( ref.current ); - }, [] ); + focusFirstTabbableIn( toolbarRef.current ); + }, [ toolbarRef ] ); const focusToolbarViaShortcut = () => { if ( shouldUseKeyboardFocusShortcut ) { @@ -121,7 +133,7 @@ function useToolbarFocus( useEffect( () => { // Store ref so we have access on useEffect cleanup: https://legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html#effect-cleanup-timing - const navigableToolbarRef = ref.current; + const navigableToolbarRef = toolbarRef.current; // If initialIndex is passed, we focus on that toolbar item when the // toolbar gets mounted and initial focus is not forced. // We have to wait for the next browser paint because block controls aren't @@ -150,32 +162,68 @@ function useToolbarFocus( const index = items.findIndex( ( item ) => item.tabIndex === 0 ); onIndexChange( index ); }; - }, [ initialIndex, initialFocusOnMount ] ); + }, [ initialIndex, initialFocusOnMount, toolbarRef ] ); + + const { lastFocus } = useSelect( ( select ) => { + const { getLastFocus } = select( blockEditorStore ); + return { + lastFocus: getLastFocus(), + }; + }, [] ); + /** + * Handles returning focus to the block editor canvas when pressing escape. + */ + useEffect( () => { + const navigableToolbarRef = toolbarRef.current; + + if ( focusEditorOnEscape ) { + const handleKeyDown = ( event ) => { + if ( event.keyCode === ESCAPE && lastFocus?.current ) { + // Focus the last focused element when pressing escape. + event.preventDefault(); + lastFocus.current.focus(); + } + }; + navigableToolbarRef.addEventListener( 'keydown', handleKeyDown ); + return () => { + navigableToolbarRef.removeEventListener( + 'keydown', + handleKeyDown + ); + }; + } + }, [ focusEditorOnEscape, lastFocus, toolbarRef ] ); } -function NavigableToolbar( { +export default function NavigableToolbar( { children, focusOnMount, + focusEditorOnEscape = false, shouldUseKeyboardFocusShortcut = true, __experimentalInitialIndex: initialIndex, __experimentalOnIndexChange: onIndexChange, ...props } ) { - const ref = useRef(); - const isAccessibleToolbar = useIsAccessibleToolbar( ref ); + const toolbarRef = useRef(); + const isAccessibleToolbar = useIsAccessibleToolbar( toolbarRef ); - useToolbarFocus( - ref, + useToolbarFocus( { + toolbarRef, focusOnMount, isAccessibleToolbar, - initialIndex, + defaultIndex: initialIndex, onIndexChange, - shouldUseKeyboardFocusShortcut - ); + shouldUseKeyboardFocusShortcut, + focusEditorOnEscape, + } ); if ( isAccessibleToolbar ) { return ( - + { children } ); @@ -185,12 +233,10 @@ function NavigableToolbar( { { children } ); } - -export default NavigableToolbar; diff --git a/packages/block-editor/src/components/plain-text/README.md b/packages/block-editor/src/components/plain-text/README.md index 25b18fcddc48fd..4e59789fd612c7 100644 --- a/packages/block-editor/src/components/plain-text/README.md +++ b/packages/block-editor/src/components/plain-text/README.md @@ -34,7 +34,7 @@ wp.blocks.registerBlockType( /* ... */, { }, edit: function( props ) { - return React.createElement( wp.editor.PlainText, { + return React.createElement( wp.blockEditor.PlainText, { className: props.className, value: props.attributes.content, onChange: function( content ) { @@ -48,8 +48,8 @@ wp.blocks.registerBlockType( /* ... */, { {% ESNext %} ```js -const { registerBlockType } = wp.blocks; -const { PlainText } = wp.editor; +import { registerBlockType } from '@wordpress/blocks'; +import { PlainText } from '@wordpress/block-editor'; registerBlockType( /* ... */, { // ... diff --git a/packages/block-editor/src/components/provider/use-block-sync.js b/packages/block-editor/src/components/provider/use-block-sync.js index 58aca847d80de0..4f2300f380892e 100644 --- a/packages/block-editor/src/components/provider/use-block-sync.js +++ b/packages/block-editor/src/components/provider/use-block-sync.js @@ -76,18 +76,11 @@ export default function useBlockSync( { resetBlocks, resetSelection, replaceInnerBlocks, - selectBlock, setHasControlledInnerBlocks, __unstableMarkNextChangeAsNotPersistent, } = registry.dispatch( blockEditorStore ); - const { - hasSelectedBlock, - getBlockName, - getBlocks, - getSelectionStart, - getSelectionEnd, - getBlock, - } = registry.select( blockEditorStore ); + const { getBlockName, getBlocks, getSelectionStart, getSelectionEnd } = + registry.select( blockEditorStore ); const isControlled = useSelect( ( select ) => { return ( @@ -180,9 +173,6 @@ export default function useBlockSync( { // bound sync, unset the outbound value to avoid considering it in // subsequent renders. pendingChanges.current.outgoing = []; - const hadSelection = hasSelectedBlock(); - const selectionAnchor = getSelectionStart(); - const selectionFocus = getSelectionEnd(); setControlledBlocks(); if ( controlledSelection ) { @@ -191,15 +181,6 @@ export default function useBlockSync( { controlledSelection.selectionEnd, controlledSelection.initialPosition ); - } else { - const selectionStillExists = getBlock( - selectionAnchor.clientId - ); - if ( hadSelection && ! selectionStillExists ) { - selectBlock( clientId ); - } else { - resetSelection( selectionAnchor, selectionFocus ); - } } } }, [ controlledBlocks, clientId ] ); diff --git a/packages/block-editor/src/components/rich-text/README.md b/packages/block-editor/src/components/rich-text/README.md index eea63d0b2912db..d17f987a34cf0e 100644 --- a/packages/block-editor/src/components/rich-text/README.md +++ b/packages/block-editor/src/components/rich-text/README.md @@ -71,7 +71,8 @@ _Optional._ A list of autocompleters to use instead of the default. ### `preserveWhiteSpace: Boolean` -_Optional._ Whether or not to preserve white space characters in the `value`. Normally tab, newline and space characters are collapsed to a single space. If turned on, soft line breaks will be saved as newline characters, not as line break elements. +_Optional._ Whether or not to preserve white space characters in the `value`. Normally tab, newline and space characters are collapsed to a single space or +trimmed. ## RichText.Content @@ -94,7 +95,7 @@ wp.blocks.registerBlockType( /* ... */, { }, edit: function( props ) { - return React.createElement( wp.editor.RichText, { + return React.createElement( wp.blockEditor.RichText, { tagName: 'h2', className: props.className, value: props.attributes.content, @@ -105,7 +106,7 @@ wp.blocks.registerBlockType( /* ... */, { }, save: function( props ) { - return React.createElement( wp.editor.RichText.Content, { + return React.createElement( wp.blockEditor.RichText.Content, { tagName: 'h2', value: props.attributes.content } ); } @@ -115,8 +116,8 @@ wp.blocks.registerBlockType( /* ... */, { {% ESNext %} ```js -const { registerBlockType } = wp.blocks; -const { RichText } = wp.editor; +import { registerBlockType } from '@wordpress/blocks'; +import { RichText } from '@wordpress/block-editor'; registerBlockType( /* ... */, { // ... @@ -161,7 +162,7 @@ wp.richText.registerFormatType( /* ... */, { /* ... */ edit: function( props ) { return React.createElement( - wp.editor.RichTextToolbarButton, { + wp.blockEditor.RichTextToolbarButton, { icon: 'editor-code', title: 'My formatting button', onClick: function() { /* ... */ } @@ -175,8 +176,8 @@ wp.richText.registerFormatType( /* ... */, { {% ESNext %} ```js -import { registerFormatType } from 'wp-rich-text'; -import { richTextToolbarButton } from 'wp-editor'; +import { registerFormatType } from '@wordpress/rich-text'; +import { RichTextToolbarButton } from '@wordpress/block-editor'; registerFormatType( /* ... */, { /* ... */ diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 3fc6ec44142225..1a6793ca9efe73 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -348,7 +348,6 @@ export function RichTextWrapper( onReplace, onSplit, __unstableEmbedURLOnPaste, - preserveWhiteSpace, pastePlainText, } ), useDelete( { diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 32bd1afd3d5404..aab10e9ab65476 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -17,7 +17,6 @@ import { } from '@wordpress/blocks'; import { useInstanceId, useMergeRefs } from '@wordpress/compose'; import { - __experimentalRichText as RichText, __unstableCreateElement, isEmpty, insert, @@ -46,6 +45,7 @@ import { } from './utils'; import EmbedHandlerPicker from './embed-handler-picker'; import { Content } from './content'; +import RichText from './native'; const classes = 'block-editor-rich-text__editable'; @@ -688,6 +688,8 @@ ForwardedRichTextContainer.Content.defaultProps = { value: '', }; +ForwardedRichTextContainer.Raw = RichText; + /** * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md */ diff --git a/packages/rich-text/src/component/format-edit.js b/packages/block-editor/src/components/rich-text/native/format-edit.js similarity index 86% rename from packages/rich-text/src/component/format-edit.js rename to packages/block-editor/src/components/rich-text/native/format-edit.js index 1867c1ef2e4f75..75b077ab321d43 100644 --- a/packages/rich-text/src/component/format-edit.js +++ b/packages/block-editor/src/components/rich-text/native/format-edit.js @@ -1,8 +1,7 @@ /** - * Internal dependencies + * WordPress dependencies */ -import { getActiveFormat } from '../get-active-format'; -import { getActiveObject } from '../get-active-object'; +import { getActiveFormat, getActiveObject } from '@wordpress/rich-text'; export default function FormatEdit( { formatTypes, diff --git a/packages/rich-text/src/get-format-colors.native.js b/packages/block-editor/src/components/rich-text/native/get-format-colors.native.js similarity index 92% rename from packages/rich-text/src/get-format-colors.native.js rename to packages/block-editor/src/components/rich-text/native/get-format-colors.native.js index f9b3a9187ca2b9..a54d3e10f78a0a 100644 --- a/packages/rich-text/src/get-format-colors.native.js +++ b/packages/block-editor/src/components/rich-text/native/get-format-colors.native.js @@ -1,7 +1,7 @@ /** - * WordPress dependencies + * Internal dependencies */ -import { getColorObjectByAttributeValues } from '@wordpress/block-editor'; +import { getColorObjectByAttributeValues } from '../../../components/colors'; const FORMAT_TYPE = 'core/text-color'; const REGEX_TO_MATCH = /^has-(.*)-color$/; diff --git a/packages/block-editor/src/components/rich-text/native/index.js b/packages/block-editor/src/components/rich-text/native/index.js new file mode 100644 index 00000000000000..2d1ec238274a0b --- /dev/null +++ b/packages/block-editor/src/components/rich-text/native/index.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/packages/rich-text/src/component/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js similarity index 98% rename from packages/rich-text/src/component/index.native.js rename to packages/block-editor/src/components/rich-text/native/index.native.js index 47b47d58c59885..2381b9809eca86 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -29,23 +29,25 @@ import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes'; import { isURL } from '@wordpress/url'; import { atSymbol, plus } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +import { + applyFormat, + getActiveFormat, + getActiveFormats, + insert, + getTextContent, + isEmpty, + create, + toHTMLString, + isCollapsed, + remove, +} from '@wordpress/rich-text'; /** * Internal dependencies */ import { useFormatTypes } from './use-format-types'; import FormatEdit from './format-edit'; -import { applyFormat } from '../apply-format'; -import { getActiveFormat } from '../get-active-format'; -import { getActiveFormats } from '../get-active-formats'; -import { insert } from '../insert'; -import { getTextContent } from '../get-text-content'; -import { isEmpty } from '../is-empty'; -import { create } from '../create'; -import { toHTMLString } from '../to-html-string'; -import { isCollapsed } from '../is-collapsed'; -import { remove } from '../remove'; -import { getFormatColors } from '../get-format-colors'; +import { getFormatColors } from './get-format-colors'; import styles from './style.scss'; import ToolbarButtonWithOptions from './toolbar-button-with-options'; diff --git a/packages/rich-text/src/component/style.native.scss b/packages/block-editor/src/components/rich-text/native/style.native.scss similarity index 100% rename from packages/rich-text/src/component/style.native.scss rename to packages/block-editor/src/components/rich-text/native/style.native.scss diff --git a/packages/rich-text/src/test/__snapshots__/index.native.js.snap b/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap similarity index 100% rename from packages/rich-text/src/test/__snapshots__/index.native.js.snap rename to packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap diff --git a/packages/rich-text/src/test/index.native.js b/packages/block-editor/src/components/rich-text/native/test/index.native.js similarity index 98% rename from packages/rich-text/src/test/index.native.js rename to packages/block-editor/src/components/rich-text/native/test/index.native.js index e0ce7ff78d6ccf..64bfb3b183c6b9 100644 --- a/packages/rich-text/src/test/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/test/index.native.js @@ -8,7 +8,7 @@ import { getEditorHtml, render, initializeEditor } from 'test/helpers'; * WordPress dependencies */ import { select } from '@wordpress/data'; -import { store as blockEditorStore } from '@wordpress/block-editor'; +import { store as richTextStore } from '@wordpress/rich-text'; import { coreBlocks } from '@wordpress/block-library'; import { getBlockTypes, @@ -19,8 +19,8 @@ import { /** * Internal dependencies */ -import { store as richTextStore } from '../store'; -import RichText from '../component/index.native'; +import { store as blockEditorStore } from '../../../../store'; +import RichText from '../index.native'; /** * Mock `useSelect` with various global application settings, e.g., styles. diff --git a/packages/rich-text/src/test/performance/rich-text.native.js b/packages/block-editor/src/components/rich-text/native/test/performance/rich-text.native.js similarity index 94% rename from packages/rich-text/src/test/performance/rich-text.native.js rename to packages/block-editor/src/components/rich-text/native/test/performance/rich-text.native.js index aaaf42c90137f9..7be9981d04bcec 100644 --- a/packages/rich-text/src/test/performance/rich-text.native.js +++ b/packages/block-editor/src/components/rich-text/native/test/performance/rich-text.native.js @@ -11,7 +11,7 @@ import { /** * Internal dependencies */ -import RichText from '../../component/index.native'; +import RichText from '../../index.native'; describe( 'RichText Performance', () => { const onCreateUndoLevel = jest.fn(); diff --git a/packages/rich-text/src/component/toolbar-button-with-options.native.js b/packages/block-editor/src/components/rich-text/native/toolbar-button-with-options.native.js similarity index 100% rename from packages/rich-text/src/component/toolbar-button-with-options.native.js rename to packages/block-editor/src/components/rich-text/native/toolbar-button-with-options.native.js diff --git a/packages/rich-text/src/component/use-format-types.js b/packages/block-editor/src/components/rich-text/native/use-format-types.js similarity index 97% rename from packages/rich-text/src/component/use-format-types.js rename to packages/block-editor/src/components/rich-text/native/use-format-types.js index 637be62b4361e0..ff65d7421ae5cc 100644 --- a/packages/rich-text/src/component/use-format-types.js +++ b/packages/block-editor/src/components/rich-text/native/use-format-types.js @@ -3,10 +3,7 @@ */ import { useMemo } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; -/** - * Internal dependencies - */ -import { store as richTextStore } from '../store'; +import { store as richTextStore } from '@wordpress/rich-text'; function formatTypesSelector( select ) { return select( richTextStore ).getFormatTypes(); diff --git a/packages/block-editor/src/components/rich-text/use-paste-handler.js b/packages/block-editor/src/components/rich-text/use-paste-handler.js index 3d24906abddd97..1302e2d0dce469 100644 --- a/packages/block-editor/src/components/rich-text/use-paste-handler.js +++ b/packages/block-editor/src/components/rich-text/use-paste-handler.js @@ -35,7 +35,6 @@ export function usePasteHandler( props ) { onReplace, onSplit, __unstableEmbedURLOnPaste, - preserveWhiteSpace, pastePlainText, } = propsRef.current; @@ -63,10 +62,7 @@ export function usePasteHandler( props ) { // without filtering the data. The filters are only meant for externally // pasted content and remove inline styles. if ( isInternal ) { - const pastedValue = create( { - html, - preserveWhiteSpace, - } ); + const pastedValue = create( { html } ); addActiveFormats( pastedValue, value.activeFormats ); onChange( insert( value, pastedValue ) ); return; @@ -136,7 +132,6 @@ export function usePasteHandler( props ) { plainText, mode, tagName, - preserveWhiteSpace, } ); if ( typeof content === 'string' ) { diff --git a/packages/block-editor/src/components/url-popover/image-url-input-ui.js b/packages/block-editor/src/components/url-popover/image-url-input-ui.js index 2f849eaad78847..7caa218658b24c 100644 --- a/packages/block-editor/src/components/url-popover/image-url-input-ui.js +++ b/packages/block-editor/src/components/url-popover/image-url-input-ui.js @@ -249,6 +249,7 @@ const ImageURLInputUI = ( { aria-expanded={ isOpen } onClick={ openLinkUI } ref={ setPopoverAnchor } + isActive={ !! url } /> { isOpen && ( select( blockEditorStore ).isNavigationMode(), [] ); + const lastFocus = useSelect( + ( select ) => select( blockEditorStore ).getLastFocus(), + [] + ); + // Don't allow tabbing to this element in Navigation mode. const focusCaptureTabIndex = ! isNavigationMode ? '0' : undefined; @@ -158,7 +163,7 @@ export default function useTabNav() { } function onFocusOut( event ) { - lastFocus.current = event.target; + setLastFocus( { ...lastFocus, current: event.target } ); const { ownerDocument } = node; diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index f9804e876ad326..563c7bae6cde93 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -98,8 +98,8 @@ export function addAttribute( settings ) { ...settings.attributes, align: { type: 'string', - // Allow for '' since it is used by updateAlignment function - // in withToolbarControls for special cases with defined default values. + // Allow for '' since it is used by the `updateAlignment` function + // in toolbar controls for special cases with defined default values. enum: [ ...ALL_ALIGNMENTS, '' ], }, }; @@ -115,7 +115,7 @@ function BlockEditAlignmentToolbarControls( { } ) { // Compute the block valid alignments by taking into account, // if the theme supports wide alignments or not and the layout's - // availble alignments. We do that for conditionally rendering + // available alignments. We do that for conditionally rendering // Slot. const blockAllowedAlignments = getValidAlignments( getBlockSupport( blockName, 'align' ), @@ -160,7 +160,7 @@ function BlockEditAlignmentToolbarControls( { * * @return {Function} Wrapped component. */ -export const withToolbarControls = createHigherOrderComponent( +export const withAlignmentControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const hasAlignmentSupport = hasBlockSupport( props.name, @@ -181,7 +181,7 @@ export const withToolbarControls = createHigherOrderComponent( ); }, - 'withToolbarControls' + 'withAlignmentControls' ); function BlockListBlockWithDataAlign( { block: BlockListBlock, props } ) { @@ -257,7 +257,7 @@ export function addAssignedAlign( props, blockType, attributes ) { addFilter( 'blocks.registerBlockType', - 'core/align/addAttribute', + 'core/editor/align/addAttribute', addAttribute ); addFilter( @@ -268,10 +268,10 @@ addFilter( addFilter( 'editor.BlockEdit', 'core/editor/align/with-toolbar-controls', - withToolbarControls + withAlignmentControls ); addFilter( 'blocks.getSaveContent.extraProps', - 'core/align/addAssignedAlign', + 'core/editor/align/addAssignedAlign', addAssignedAlign ); diff --git a/packages/block-editor/src/hooks/align.native.js b/packages/block-editor/src/hooks/align.native.js index 75b25b8dca5a6b..1bf375b654ad40 100644 --- a/packages/block-editor/src/hooks/align.native.js +++ b/packages/block-editor/src/hooks/align.native.js @@ -36,8 +36,8 @@ addFilter( ...settings.attributes, align: { type: 'string', - // Allow for '' since it is used by updateAlignment function - // in withToolbarControls for special cases with defined default values. + // Allow for '' since it is used by the `updateAlignment` function + // in toolbar controls for special cases with defined default values. enum: [ ...ALIGNMENTS, '' ], }, }; diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 297fcc49d2123c..3d404c4a868116 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -59,6 +59,7 @@ function BlockEditAnchorControl( { blockName, attributes, setAttributes } ) { const textControl = ( { - return ( props ) => { - return ( - <> - - { props.isSelected && - hasBlockSupport( props.name, 'anchor' ) && ( - - ) } - - ); - }; - }, - 'withInspectorControl' -); +export const withAnchorControls = createHigherOrderComponent( ( BlockEdit ) => { + return ( props ) => { + return ( + <> + + { props.isSelected && + hasBlockSupport( props.name, 'anchor' ) && ( + + ) } + + ); + }; +}, 'withAnchorControls' ); /** * Override props assigned to save component to inject anchor ID, if block @@ -166,11 +164,11 @@ export function addSaveProps( extraProps, blockType, attributes ) { addFilter( 'blocks.registerBlockType', 'core/anchor/attribute', addAttribute ); addFilter( 'editor.BlockEdit', - 'core/editor/anchor/with-inspector-control', - withInspectorControl + 'core/editor/anchor/with-inspector-controls', + withAnchorControls ); addFilter( 'blocks.getSaveContent.extraProps', - 'core/anchor/save-props', + 'core/editor/anchor/save-props', addSaveProps ); diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index f78341e16df8e0..b0f93fa8b2e060 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; */ import { isBlobURL } from '@wordpress/blob'; import { getBlockSupport } from '@wordpress/blocks'; +import { focus } from '@wordpress/dom'; import { __experimentalToolsPanelItem as ToolsPanelItem, DropZone, @@ -19,7 +20,7 @@ import { __experimentalTruncate as Truncate, } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; -import { Platform, useCallback } from '@wordpress/element'; +import { Platform, useCallback, useRef } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { getFilename } from '@wordpress/url'; @@ -150,6 +151,8 @@ function BackgroundImagePanelItem( props ) { const { id, title, url } = attributes.style?.background?.backgroundImage || {}; + const replaceContainerRef = useRef(); + const { mediaUpload } = useSelect( ( select ) => { return { mediaUpload: select( blockEditorStore ).getSettings().mediaUpload, @@ -241,17 +244,22 @@ function BackgroundImagePanelItem( props ) { }; }, [] ); + const hasValue = hasBackgroundImageValue( props ); + return ( hasBackgroundImageValue( props ) } + hasValue={ () => hasValue } label={ __( 'Background image' ) } onDeselect={ () => resetBackgroundImage( props ) } isShownByDefault={ true } resetAllFilter={ resetAllFilter } panelId={ clientId } > -
    +
    - resetBackgroundImage( props ) }> - { __( 'Reset ' ) } - + { hasValue && ( + { + const [ toggleButton ] = focus.tabbable.find( + replaceContainerRef.current + ); + // Focus the toggle button and close the dropdown menu. + // This ensures similar behaviour as to selecting an image, where the dropdown is + // closed and focus is redirected to the dropdown toggle button. + toggleButton?.focus(); + toggleButton?.click(); + resetBackgroundImage( props ); + } } + > + { __( 'Reset ' ) } + + ) } { - return ( props ) => { - const blockEdit = ; - return ( - <> - { blockEdit } - - - ); - }; -}, 'withBlockHooks' ); +export const withBlockHooksControls = createHigherOrderComponent( + ( BlockEdit ) => { + return ( props ) => { + return ( + <> + + { props.isSelected && ( + + ) } + + ); + }; + }, + 'withBlockHooksControls' +); addFilter( 'editor.BlockEdit', - 'core/block-hooks/with-inspector-control', - withBlockHooks + 'core/editor/block-hooks/with-inspector-controls', + withBlockHooksControls ); diff --git a/packages/block-editor/src/hooks/block-rename-ui.js b/packages/block-editor/src/hooks/block-rename-ui.js index f1e488eca03a32..836df953256c10 100644 --- a/packages/block-editor/src/hooks/block-rename-ui.js +++ b/packages/block-editor/src/hooks/block-rename-ui.js @@ -92,6 +92,7 @@ function RenameModal( { blockName, originalBlockName, onClose, onSave } ) { ( props ) => { const { clientId, name, attributes, setAttributes, isSelected } = props; @@ -216,11 +218,11 @@ export const withBlockRenameControl = createHigherOrderComponent( ); }, - 'withToolbarControls' + 'withBlockRenameControls' ); addFilter( 'editor.BlockEdit', - 'core/block-rename-ui/with-block-rename-control', - withBlockRenameControl + 'core/block-rename-ui/with-block-rename-controls', + withBlockRenameControls ); diff --git a/packages/block-editor/src/hooks/content-lock-ui.js b/packages/block-editor/src/hooks/content-lock-ui.js index c1cf7016bfd443..5d277d6a516d2d 100644 --- a/packages/block-editor/src/hooks/content-lock-ui.js +++ b/packages/block-editor/src/hooks/content-lock-ui.js @@ -37,7 +37,7 @@ function StopEditingAsBlocksOnOutsideSelect( { return null; } -export const withBlockControls = createHigherOrderComponent( +export const withContentLockControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { getBlockListSettings, getSettings } = useSelect( blockEditorStore ); @@ -155,11 +155,11 @@ export const withBlockControls = createHigherOrderComponent( ); }, - 'withToolbarControls' + 'withContentLockControls' ); addFilter( 'editor.BlockEdit', 'core/content-lock-ui/with-block-controls', - withBlockControls + withContentLockControls ); diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js index dd4f4cd8c51e2b..8a3becc8691421 100644 --- a/packages/block-editor/src/hooks/custom-class-name.js +++ b/packages/block-editor/src/hooks/custom-class-name.js @@ -49,6 +49,7 @@ function CustomClassNameControls( { attributes, setAttributes } ) { { return ( props ) => { const hasCustomClassName = hasBlockSupport( @@ -94,7 +95,7 @@ export const withInspectorControl = createHigherOrderComponent( ); }; }, - 'withInspectorControl' + 'withCustomClassNameControls' ); /** @@ -163,17 +164,17 @@ export function addTransforms( result, source, index, results ) { addFilter( 'blocks.registerBlockType', - 'core/custom-class-name/attribute', + 'core/editor/custom-class-name/attribute', addAttribute ); addFilter( 'editor.BlockEdit', - 'core/editor/custom-class-name/with-inspector-control', - withInspectorControl + 'core/editor/custom-class-name/with-inspector-controls', + withCustomClassNameControls ); addFilter( 'blocks.getSaveContent.extraProps', - 'core/custom-class-name/save-props', + 'core/editor/custom-class-name/save-props', addSaveProps ); diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 9affb55c3ea71f..31721569781b6c 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -44,7 +44,7 @@ function addAttribute( settings ) { * * @return {Component} Wrapped component. */ -const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { +const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { const blockEditingMode = useBlockEditingMode(); const hasCustomFieldsSupport = hasBlockSupport( @@ -123,17 +123,17 @@ const withInspectorControl = createHigherOrderComponent( ( BlockEdit ) => { return ; }; -}, 'withInspectorControl' ); +}, 'withCustomFieldsControls' ); if ( window.__experimentalConnections ) { addFilter( 'blocks.registerBlockType', - 'core/connections/attribute', + 'core/editor/connections/attribute', addAttribute ); addFilter( 'editor.BlockEdit', - 'core/connections/with-inspector-control', - withInspectorControl + 'core/editor/connections/with-inspector-controls', + withCustomFieldsControls ); } diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index 5470ea5789f3a8..5442e394e68c78 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -16,7 +16,6 @@ import { import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useEffect } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -34,12 +33,10 @@ import { } from '../components/duotone/utils'; import { getBlockCSSSelector } from '../components/global-styles/get-block-css-selector'; import { scopeSelector } from '../components/global-styles/utils'; -import { useBlockSettings } from './utils'; +import { useBlockSettings, useStyleOverride } from './utils'; import { default as StylesFiltersPanel } from '../components/global-styles/filters-panel'; import { useBlockEditingMode } from '../components/block-editing-mode'; import { __unstableUseBlockElement as useBlockElement } from '../components/block-list/use-block-props/use-block-refs'; -import { store as blockEditorStore } from '../store'; -import { unlock } from '../lock-unlock'; const EMPTY_ARRAY = []; @@ -291,8 +288,27 @@ function DuotoneStyles( { const isValidFilter = Array.isArray( colors ) || colors === 'unset'; - const { setStyleOverride, deleteStyleOverride } = unlock( - useDispatch( blockEditorStore ) + useStyleOverride( + isValidFilter + ? { + css: + colors !== 'unset' + ? getDuotoneStylesheet( selector, filterId ) + : getDuotoneUnsetStylesheet( selector ), + __unstableType: 'presets', + } + : undefined + ); + useStyleOverride( + isValidFilter + ? { + assets: + colors !== 'unset' + ? getDuotoneFilter( filterId, colors ) + : '', + __unstableType: 'svgs', + } + : undefined ); const blockElement = useBlockElement( clientId ); @@ -300,19 +316,6 @@ function DuotoneStyles( { useEffect( () => { if ( ! isValidFilter ) return; - setStyleOverride( filterId, { - css: - colors !== 'unset' - ? getDuotoneStylesheet( selector, filterId ) - : getDuotoneUnsetStylesheet( selector ), - __unstableType: 'presets', - } ); - setStyleOverride( `duotone-${ filterId }`, { - assets: - colors !== 'unset' ? getDuotoneFilter( filterId, colors ) : '', - __unstableType: 'svgs', - } ); - // Safari does not always update the duotone filter when the duotone colors // are changed. When using Safari, force the block element to be repainted by // the browser to ensure any changes are reflected visually. This logic matches @@ -329,20 +332,7 @@ function DuotoneStyles( { blockElement.offsetHeight; blockElement.style.display = display; } - - return () => { - deleteStyleOverride( filterId ); - deleteStyleOverride( `duotone-${ filterId }` ); - }; - }, [ - isValidFilter, - blockElement, - colors, - selector, - filterId, - setStyleOverride, - deleteStyleOverride, - ] ); + }, [ isValidFilter, blockElement ] ); return null; } diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 068af349d467dc..f4730702e9adb7 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -9,7 +9,7 @@ import classnames from 'classnames'; import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { Button, ButtonGroup, @@ -17,7 +17,6 @@ import { PanelBody, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -29,8 +28,7 @@ import { getLayoutType, getLayoutTypes } from '../layouts'; import { useBlockEditingMode } from '../components/block-editing-mode'; import { LAYOUT_DEFINITIONS } from '../layouts/definitions'; import { kebabCase } from '../utils/object'; -import { useBlockSettings } from './utils'; -import { unlock } from '../lock-unlock'; +import { useBlockSettings, useStyleOverride } from './utils'; const layoutBlockSupportKey = 'layout'; @@ -333,7 +331,7 @@ export function addAttribute( settings ) { * * @return {Function} Wrapped component. */ -export const withInspectorControls = createHigherOrderComponent( +export const withLayoutControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const supportLayout = hasLayoutBlockSupport( props.name ); @@ -342,9 +340,55 @@ export const withInspectorControls = createHigherOrderComponent( , ]; }, - 'withInspectorControls' + 'withLayoutControls' ); +function BlockWithLayoutStyles( { block: BlockListBlock, props } ) { + const { name, attributes } = props; + const id = useInstanceId( BlockListBlock ); + const { layout } = attributes; + const { default: defaultBlockLayout } = + getBlockSupport( name, layoutBlockSupportKey ) || {}; + const usedLayout = + layout?.inherit || layout?.contentSize || layout?.wideSize + ? { ...layout, type: 'constrained' } + : layout || defaultBlockLayout || {}; + const layoutClasses = useLayoutClasses( attributes, name ); + + // Higher specificity to override defaults from theme.json. + const selector = `.wp-container-${ id }.wp-container-${ id }`; + const [ blockGapSupport ] = useSettings( 'spacing.blockGap' ); + const hasBlockGapSupport = blockGapSupport !== null; + + // Get CSS string for the current layout type. + // The CSS and `style` element is only output if it is not empty. + const fullLayoutType = getLayoutType( usedLayout?.type || 'default' ); + const css = fullLayoutType?.getLayoutStyle?.( { + blockName: name, + selector, + layout: usedLayout, + style: attributes?.style, + hasBlockGapSupport, + } ); + + // Attach a `wp-container-` id-based class name as well as a layout class name such as `is-layout-flex`. + const layoutClassNames = classnames( + { + [ `wp-container-${ id }` ]: !! css, // Only attach a container class if there is generated CSS to be attached. + }, + layoutClasses + ); + + useStyleOverride( { css } ); + + return ( + + ); +} + /** * Override the default block element to add the layout styles. * @@ -354,76 +398,60 @@ export const withInspectorControls = createHigherOrderComponent( */ export const withLayoutStyles = createHigherOrderComponent( ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; - const blockSupportsLayout = hasLayoutBlockSupport( name ); - const disableLayoutStyles = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); - return !! getSettings().disableLayoutStyles; - } ); - const shouldRenderLayoutStyles = - blockSupportsLayout && ! disableLayoutStyles; - const id = useInstanceId( BlockListBlock ); - const { layout } = attributes; - const { default: defaultBlockLayout } = - getBlockSupport( name, layoutBlockSupportKey ) || {}; - const usedLayout = - layout?.inherit || layout?.contentSize || layout?.wideSize - ? { ...layout, type: 'constrained' } - : layout || defaultBlockLayout || {}; - const layoutClasses = blockSupportsLayout - ? useLayoutClasses( attributes, name ) - : null; - // Higher specificity to override defaults from theme.json. - const selector = `.wp-container-${ id }.wp-container-${ id }`; - const [ blockGapSupport ] = useSettings( 'spacing.blockGap' ); - const hasBlockGapSupport = blockGapSupport !== null; - - // Get CSS string for the current layout type. - // The CSS and `style` element is only output if it is not empty. - let css; - if ( shouldRenderLayoutStyles ) { - const fullLayoutType = getLayoutType( - usedLayout?.type || 'default' - ); - css = fullLayoutType?.getLayoutStyle?.( { - blockName: name, - selector, - layout: usedLayout, - style: attributes?.style, - hasBlockGapSupport, - } ); - } - - // Attach a `wp-container-` id-based class name as well as a layout class name such as `is-layout-flex`. - const layoutClassNames = classnames( - { - [ `wp-container-${ id }` ]: shouldRenderLayoutStyles && !! css, // Only attach a container class if there is generated CSS to be attached. + const blockSupportsLayout = hasLayoutBlockSupport( props.name ); + const shouldRenderLayoutStyles = useSelect( + ( select ) => { + // The callback returns early to avoid block editor subscription. + if ( ! blockSupportsLayout ) { + return false; + } + + return ! select( blockEditorStore ).getSettings() + .disableLayoutStyles; }, - layoutClasses + [ blockSupportsLayout ] ); - const { setStyleOverride, deleteStyleOverride } = unlock( - useDispatch( blockEditorStore ) - ); - - useEffect( () => { - if ( ! css ) return; - setStyleOverride( selector, { css } ); - return () => { - deleteStyleOverride( selector ); - }; - }, [ selector, css, setStyleOverride, deleteStyleOverride ] ); + if ( ! shouldRenderLayoutStyles ) { + return ; + } return ( - + ); }, 'withLayoutStyles' ); +function BlockWithChildLayoutStyles( { block: BlockListBlock, props } ) { + const layout = props.attributes.style?.layout ?? {}; + const { selfStretch, flexSize } = layout; + + const id = useInstanceId( BlockListBlock ); + const selector = `.wp-container-content-${ id }`; + + let css = ''; + if ( selfStretch === 'fixed' && flexSize ) { + css = `${ selector } { + flex-basis: ${ flexSize }; + box-sizing: border-box; + }`; + } else if ( selfStretch === 'fill' ) { + css = `${ selector } { + flex-grow: 1; + }`; + } + + // Attach a `wp-container-content` id-based classname. + const className = classnames( props.className, { + [ `wp-container-content-${ id }` ]: !! css, // Only attach a container class if there is generated CSS to be attached. + } ); + + useStyleOverride( { css } ); + + return ; +} + /** * Override the default block element to add the child layout styles. * @@ -433,52 +461,33 @@ export const withLayoutStyles = createHigherOrderComponent( */ export const withChildLayoutStyles = createHigherOrderComponent( ( BlockListBlock ) => ( props ) => { - const { attributes } = props; - const { style: { layout = {} } = {} } = attributes; + const layout = props.attributes.style?.layout ?? {}; const { selfStretch, flexSize } = layout; const hasChildLayout = selfStretch || flexSize; - const disableLayoutStyles = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); - return !! getSettings().disableLayoutStyles; - } ); - const shouldRenderChildLayoutStyles = - hasChildLayout && ! disableLayoutStyles; - const id = useInstanceId( BlockListBlock ); - const selector = `.wp-container-content-${ id }`; + const shouldRenderChildLayoutStyles = useSelect( + ( select ) => { + // The callback returns early to avoid block editor subscription. + if ( ! hasChildLayout ) { + return false; + } - let css = ''; + return ! select( blockEditorStore ).getSettings() + .disableLayoutStyles; + }, + [ hasChildLayout ] + ); - if ( selfStretch === 'fixed' && flexSize ) { - css += `${ selector } { - flex-basis: ${ flexSize }; - box-sizing: border-box; - }`; - } else if ( selfStretch === 'fill' ) { - css += `${ selector } { - flex-grow: 1; - }`; + if ( ! shouldRenderChildLayoutStyles ) { + return ; } - // Attach a `wp-container-content` id-based classname. - const className = classnames( props?.className, { - [ `wp-container-content-${ id }` ]: - shouldRenderChildLayoutStyles && !! css, // Only attach a container class if there is generated CSS to be attached. - } ); - - const { setStyleOverride, deleteStyleOverride } = unlock( - useDispatch( blockEditorStore ) + return ( + ); - - useEffect( () => { - if ( ! css ) return; - setStyleOverride( selector, { css } ); - return () => { - deleteStyleOverride( selector ); - }; - }, [ selector, css, setStyleOverride, deleteStyleOverride ] ); - - return ; }, 'withChildLayoutStyles' ); @@ -501,5 +510,5 @@ addFilter( addFilter( 'editor.BlockEdit', 'core/editor/layout/with-inspector-controls', - withInspectorControls + withLayoutControls ); diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index d040a2c39d21d0..710dbfaf5ace04 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -14,22 +14,16 @@ import { } from '@wordpress/components'; import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; -import { - useContext, - useMemo, - createPortal, - Platform, -} from '@wordpress/element'; +import { useMemo, Platform } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies */ -import BlockList from '../components/block-list'; import { useSettings } from '../components/use-settings'; import InspectorControls from '../components/inspector-controls'; import useBlockDisplayInformation from '../components/use-block-display-information'; -import { cleanEmptyObject } from './utils'; +import { cleanEmptyObject, useStyleOverride } from './utils'; import { unlock } from '../lock-unlock'; import { store as blockEditorStore } from '../store'; @@ -329,7 +323,7 @@ export function PositionPanel( props ) { * * @return {Function} Wrapped component. */ -export const withInspectorControls = createHigherOrderComponent( +export const withPositionControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { name: blockName } = props; const positionSupport = hasBlockSupport( @@ -346,7 +340,7 @@ export const withInspectorControls = createHigherOrderComponent( , ]; }, - 'withInspectorControls' + 'withPositionControls' ); /** @@ -368,7 +362,6 @@ export const withPositionStyles = createHigherOrderComponent( hasPositionBlockSupport && ! isPositionDisabled; const id = useInstanceId( BlockListBlock ); - const element = useContext( BlockList.__unstableElementContext ); // Higher specificity to override defaults in editor UI. const positionSelector = `.wp-container-${ id }.wp-container-${ id }`; @@ -392,15 +385,9 @@ export const withPositionStyles = createHigherOrderComponent( !! attributes?.style?.position?.type, } ); - return ( - <> - { allowPositionStyles && - element && - !! css && - createPortal( , element ) } - - - ); + useStyleOverride( { css } ); + + return ; }, 'withPositionStyles' ); @@ -413,5 +400,5 @@ addFilter( addFilter( 'editor.BlockEdit', 'core/editor/position/with-inspector-controls', - withInspectorControls + withPositionControls ); diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 9c95f0a12996e1..d74e10b0208f1c 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useContext, useMemo, createPortal } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, @@ -19,7 +19,6 @@ import { getCSSRules, compileCSS } from '@wordpress/style-engine'; /** * Internal dependencies */ -import BlockList from '../components/block-list'; import { BACKGROUND_SUPPORT_KEY, BackgroundImagePanel } from './background'; import { BORDER_SUPPORT_KEY, BorderPanel } from './border'; import { COLOR_SUPPORT_KEY, ColorEdit } from './color'; @@ -34,7 +33,7 @@ import { DimensionsPanel, } from './dimensions'; import useDisplayBlockControls from '../components/use-display-block-controls'; -import { shouldSkipSerialization } from './utils'; +import { shouldSkipSerialization, useStyleOverride } from './utils'; import { scopeSelector } from '../components/global-styles/utils'; import { useBlockEditingMode } from '../components/block-editing-mode'; @@ -354,7 +353,7 @@ export function addEditProps( settings ) { * * @return {Function} Wrapped component. */ -export const withBlockControls = createHigherOrderComponent( +export const withBlockStyleControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { if ( ! hasStyleSupport( props.name ) ) { return ; @@ -378,7 +377,7 @@ export const withBlockControls = createHigherOrderComponent( ); }, - 'withToolbarControls' + 'withBlockStyleControls' ); // Defines which element types are supported, including their hover styles or @@ -484,33 +483,20 @@ const withElementsStyles = createHigherOrderComponent( : undefined; }, [ baseElementSelector, blockElementStyles, props.name ] ); - const element = useContext( BlockList.__unstableElementContext ); + useStyleOverride( { css: styles } ); return ( - <> - { styles && - element && - createPortal( - ; - }; + useStyleOverride( { css: gap } ); - return gap && styleElement - ? createPortal( , styleElement ) - : null; + return null; } diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index 4d5354eff0180f..277fa6872fa82e 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -52,6 +52,7 @@ function GroupEditControls( { tagName, onSelectTagName } ) { )' ), value: 'div' }, diff --git a/packages/block-library/src/html/transforms.js b/packages/block-library/src/html/transforms.js index 6265acefc8f80f..af1a16288fe9fc 100644 --- a/packages/block-library/src/html/transforms.js +++ b/packages/block-library/src/html/transforms.js @@ -2,15 +2,18 @@ * WordPress dependencies */ import { createBlock } from '@wordpress/blocks'; +import { create } from '@wordpress/rich-text'; const transforms = { from: [ { type: 'block', blocks: [ 'core/code' ], - transform: ( { content } ) => { + transform: ( { content: html } ) => { return createBlock( 'core/html', { - content, + // The code block may output HTML formatting, so convert it + // to plain text. + content: create( { html } ).text, } ); }, }, diff --git a/packages/block-library/src/image/deprecated.js b/packages/block-library/src/image/deprecated.js index 4205da8e117b91..0365ddcfff5d17 100644 --- a/packages/block-library/src/image/deprecated.js +++ b/packages/block-library/src/image/deprecated.js @@ -1047,6 +1047,14 @@ const v8 = { }, }, migrate( { width, height, ...attributes } ) { + // We need to perform a check here because in cases + // where attributes are added dynamically to blocks, + // block invalidation overrides the isEligible() method + // and forces the migration to run, so it's not guaranteed + // that `behaviors` or `behaviors.lightbox` will be defined. + if ( ! attributes.behaviors?.lightbox ) { + return attributes; + } const { behaviors: { lightbox: { enabled }, diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index 934682ed91b7de..e1721928362149 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -62,6 +62,13 @@ figure.wp-block-image:not(.wp-block) { left: 50%; transform: translate(-50%, -50%); } + + // When the Image block is linked, + // it's wrapped with a disabled tag. + // Restore cursor style so it doesn't appear 'clickable'. + > a { + cursor: default; + } } // This is necessary for the editor resize handles to accurately work on a non-floated, non-resized, small image. diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index c465677a986e05..acefd5714bbd47 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -242,10 +242,9 @@ class="lightbox-trigger" data-wp-on--click="actions.core.image.showLightbox" data-wp-style--right="context.core.image.imageButtonRight" data-wp-style--top="context.core.image.imageButtonTop" - style="background: #000" > -
  • `. - $w->set_attribute( 'data-wp-interactive', true ); - $w->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "submenuOpenedBy": {}, "type": "submenu" } } }' ); - $w->set_attribute( 'data-wp-effect', 'effects.core.navigation.initMenu' ); - $w->set_attribute( 'data-wp-on--focusout', 'actions.core.navigation.handleMenuFocusout' ); - $w->set_attribute( 'data-wp-on--keydown', 'actions.core.navigation.handleMenuKeydown' ); + $tags->set_attribute( 'data-wp-interactive', true ); + $tags->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "submenuOpenedBy": {}, "type": "submenu" } } }' ); + $tags->set_attribute( 'data-wp-effect', 'effects.core.navigation.initMenu' ); + $tags->set_attribute( 'data-wp-on--focusout', 'actions.core.navigation.handleMenuFocusout' ); + $tags->set_attribute( 'data-wp-on--keydown', 'actions.core.navigation.handleMenuKeydown' ); // This is a fix for Safari. Without it, Safari doesn't change the active // element when the user clicks on a button. It can be removed once we add // an overlay to capture the clicks, instead of relying on the focusout // event. - $w->set_attribute( 'tabindex', '-1' ); + $tags->set_attribute( 'tabindex', '-1' ); if ( ! isset( $block_attributes['openSubmenusOnClick'] ) || false === $block_attributes['openSubmenusOnClick'] ) { - $w->set_attribute( 'data-wp-on--mouseenter', 'actions.core.navigation.openMenuOnHover' ); - $w->set_attribute( 'data-wp-on--mouseleave', 'actions.core.navigation.closeMenuOnHover' ); + $tags->set_attribute( 'data-wp-on--mouseenter', 'actions.core.navigation.openMenuOnHover' ); + $tags->set_attribute( 'data-wp-on--mouseleave', 'actions.core.navigation.closeMenuOnHover' ); } // Add directives to the toggle submenu button. - if ( $w->next_tag( + if ( $tags->next_tag( array( 'tag_name' => 'BUTTON', 'class_name' => 'wp-block-navigation-submenu__toggle', ) ) ) { - $w->set_attribute( 'data-wp-on--click', 'actions.core.navigation.toggleMenuOnClick' ); - $w->set_attribute( 'data-wp-bind--aria-expanded', 'selectors.core.navigation.isMenuOpen' ); + $tags->set_attribute( 'data-wp-on--click', 'actions.core.navigation.toggleMenuOnClick' ); + $tags->set_attribute( 'data-wp-bind--aria-expanded', 'selectors.core.navigation.isMenuOpen' ); // The `aria-expanded` attribute for SSR is already added in the submenu block. } // Add directives to the submenu. - if ( $w->next_tag( + if ( $tags->next_tag( array( 'tag_name' => 'UL', 'class_name' => 'wp-block-navigation__submenu-container', ) ) ) { - $w->set_attribute( 'data-wp-on--focus', 'actions.core.navigation.openMenuOnFocus' ); + $tags->set_attribute( 'data-wp-on--focus', 'actions.core.navigation.openMenuOnFocus' ); } // Iterate through subitems if exist. - block_core_navigation_add_directives_to_submenu( $w, $block_attributes ); + block_core_navigation_add_directives_to_submenu( $tags, $block_attributes ); } - return $w->get_updated_html(); + return $tags->get_updated_html(); } /** @@ -391,391 +407,10 @@ function block_core_navigation_from_block_get_post_ids( $block ) { * @param string $content The saved content. * @param WP_Block $block The parsed block. * - * @return string Returns the post content with the legacy widget added. + * @return string Returns the navigation block markup. */ function render_block_core_navigation( $attributes, $content, $block ) { - static $seen_menu_names = array(); - - // Flag used to indicate whether the rendered output is considered to be - // a fallback (i.e. the block has no menu associated with it). - $is_fallback = false; - - $nav_menu_name = $attributes['ariaLabel'] ?? ''; - - /** - * Deprecated: - * The rgbTextColor and rgbBackgroundColor attributes - * have been deprecated in favor of - * customTextColor and customBackgroundColor ones. - * Move the values from old attrs to the new ones. - */ - if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) { - $attributes['customTextColor'] = $attributes['rgbTextColor']; - } - - if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) { - $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor']; - } - - unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] ); - - /** - * This is for backwards compatibility after `isResponsive` attribute has been removed. - */ - $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive']; - $is_responsive_menu = isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute; - - $inner_blocks = $block->inner_blocks; - - // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render. - if ( array_key_exists( 'navigationMenuId', $attributes ) ) { - $attributes['ref'] = $attributes['navigationMenuId']; - } - - // If: - // - the gutenberg plugin is active - // - `__unstableLocation` is defined - // - we have menu items at the defined location - // - we don't have a relationship to a `wp_navigation` Post (via `ref`). - // ...then create inner blocks from the classic menu assigned to that location. - if ( - defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && - array_key_exists( '__unstableLocation', $attributes ) && - ! array_key_exists( 'ref', $attributes ) && - ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) - ) { - $menu_items = block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ); - if ( empty( $menu_items ) ) { - return ''; - } - - $menu_items_by_parent_id = block_core_navigation_sort_menu_items_by_parent_id( $menu_items ); - $parsed_blocks = block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id ); - $inner_blocks = new WP_Block_List( $parsed_blocks, $attributes ); - } - - // Load inner blocks from the navigation post. - if ( array_key_exists( 'ref', $attributes ) ) { - $navigation_post = get_post( $attributes['ref'] ); - if ( ! isset( $navigation_post ) ) { - return ''; - } - - // Only published posts are valid. If this is changed then a corresponding change - // must also be implemented in `use-navigation-menu.js`. - if ( 'publish' === $navigation_post->post_status ) { - $nav_menu_name = $navigation_post->post_title; - - if ( isset( $seen_menu_names[ $nav_menu_name ] ) ) { - ++$seen_menu_names[ $nav_menu_name ]; - } else { - $seen_menu_names[ $nav_menu_name ] = 1; - } - - $parsed_blocks = parse_blocks( $navigation_post->post_content ); - - // 'parse_blocks' includes a null block with '\n\n' as the content when - // it encounters whitespace. This code strips it. - $compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); - - // TODO - this uses the full navigation block attributes for the - // context which could be refined. - $inner_blocks = new WP_Block_List( $compacted_blocks, $attributes ); - } - } - - // If there are no inner blocks then fallback to rendering an appropriate fallback. - if ( empty( $inner_blocks ) ) { - $is_fallback = true; // indicate we are rendering the fallback. - - $fallback_blocks = block_core_navigation_get_fallback_blocks(); - - // Fallback my have been filtered so do basic test for validity. - if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { - return ''; - } - - $inner_blocks = new WP_Block_List( $fallback_blocks, $attributes ); - } - - if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { - return ''; - } - - /** - * Filter navigation block $inner_blocks. - * Allows modification of a navigation block menu items. - * - * @since 6.1.0 - * - * @param \WP_Block_List $inner_blocks - */ - $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); - - $layout_justification = array( - 'left' => 'items-justified-left', - 'right' => 'items-justified-right', - 'center' => 'items-justified-center', - 'space-between' => 'items-justified-space-between', - ); - - // Restore legacy classnames for submenu positioning. - $layout_class = ''; - if ( - isset( $attributes['layout']['justifyContent'] ) && - isset( $layout_justification[ $attributes['layout']['justifyContent'] ] ) - ) { - $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ]; - } - if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) { - $layout_class .= ' is-vertical'; - } - - if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) { - $layout_class .= ' no-wrap'; - } - - // Manually add block support text decoration as CSS class. - $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; - $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); - - $colors = block_core_navigation_build_css_colors( $attributes ); - $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); - $classes = array_merge( - $colors['css_classes'], - $font_sizes['css_classes'], - $is_responsive_menu ? array( 'is-responsive' ) : array(), - $layout_class ? array( $layout_class ) : array(), - $is_fallback ? array( 'is-fallback' ) : array(), - $text_decoration ? array( $text_decoration_class ) : array() - ); - - $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); - if ( $post_ids ) { - _prime_post_caches( $post_ids, false, false ); - } - - $list_item_nav_blocks = array( - 'core/navigation-link', - 'core/home-link', - 'core/site-title', - 'core/site-logo', - 'core/navigation-submenu', - ); - - $needs_list_item_wrapper = array( - 'core/site-title', - 'core/site-logo', - ); - - $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; - $style = $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; - $class = implode( ' ', $classes ); - - // If the menu name has been used previously then append an ID - // to the name to ensure uniqueness across a given post. - if ( isset( $seen_menu_names[ $nav_menu_name ] ) && $seen_menu_names[ $nav_menu_name ] > 1 ) { - $count = $seen_menu_names[ $nav_menu_name ]; - $nav_menu_name = $nav_menu_name . ' ' . ( $count ); - } - - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'class' => $class, - 'style' => $style, - 'aria-label' => $nav_menu_name, - ) - ); - - $container_attributes = get_block_wrapper_attributes( - array( - 'class' => 'wp-block-navigation__container ' . $class, - 'style' => $style, - ) - ); - - $inner_blocks_html = ''; - $is_list_open = false; - $has_submenus = false; - foreach ( $inner_blocks as $inner_block ) { - $is_list_item = in_array( $inner_block->name, $list_item_nav_blocks, true ); - - if ( $is_list_item && ! $is_list_open ) { - $is_list_open = true; - $inner_blocks_html .= sprintf( - '
      ', - $container_attributes - ); - } - - if ( ! $is_list_item && $is_list_open ) { - $is_list_open = false; - $inner_blocks_html .= '
    '; - } - - $inner_block_content = $inner_block->render(); - $p = new WP_HTML_Tag_Processor( $inner_block_content ); - if ( $p->next_tag( - array( - 'name' => 'LI', - 'class_name' => 'has-child', - ) - ) ) { - $has_submenus = true; - } - if ( ! empty( $inner_block_content ) ) { - if ( in_array( $inner_block->name, $needs_list_item_wrapper, true ) ) { - $inner_blocks_html .= '
  • ' . $inner_block_content . '
  • '; - } else { - $inner_blocks_html .= $inner_block_content; - } - } - } - - if ( $is_list_open ) { - $inner_blocks_html .= ''; - } - - $should_load_view_script = ( $has_submenus && ( $attributes['openSubmenusOnClick'] || $attributes['showSubmenuIcon'] ) ) || $is_responsive_menu; - $view_js_file = 'wp-block-navigation-view'; - - // If the script already exists, there is no point in removing it from viewScript. - if ( ! wp_script_is( $view_js_file ) ) { - $script_handles = $block->block_type->view_script_handles; - - // If the script is not needed, and it is still in the `view_script_handles`, remove it. - if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) ); - } - // If the script is needed, but it was previously removed, add it again. - if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) { - $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) ); - } - } - - // Add directives to the submenu if needed. - if ( $has_submenus && $should_load_view_script ) { - $w = new WP_HTML_Tag_Processor( $inner_blocks_html ); - $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $w, $attributes ); - } - - $modal_unique_id = wp_unique_id( 'modal-' ); - - // Determine whether or not navigation elements should be wrapped in the markup required to make it responsive, - // return early if they don't. - if ( ! $is_responsive_menu ) { - return sprintf( - '', - $wrapper_attributes, - $inner_blocks_html - ); - } - - $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; - - $responsive_container_classes = array( - 'wp-block-navigation__responsive-container', - $is_hidden_by_default ? 'hidden-by-default' : '', - implode( ' ', $colors['overlay_css_classes'] ), - ); - $open_button_classes = array( - 'wp-block-navigation__responsive-container-open', - $is_hidden_by_default ? 'always-shown' : '', - ); - - $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon']; - $toggle_button_icon = ''; - if ( isset( $attributes['icon'] ) ) { - if ( 'menu' === $attributes['icon'] ) { - $toggle_button_icon = ''; - } - } - $toggle_button_content = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' ); - $toggle_close_button_icon = ''; - $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' ); - $toggle_aria_label_open = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label. - $toggle_aria_label_close = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label. - - // Add Interactivity API directives to the markup if needed. - $nav_element_directives = ''; - $open_button_directives = ''; - $responsive_container_directives = ''; - $responsive_dialog_directives = ''; - $close_button_directives = ''; - if ( $should_load_view_script ) { - $nav_element_context = wp_json_encode( - array( - 'core' => array( - 'navigation' => array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), - ), - ), - ), - JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP - ); - $nav_element_directives = ' - data-wp-interactive - data-wp-context=\'' . $nav_element_context . '\' - '; - $open_button_directives = ' - data-wp-on--click="actions.core.navigation.openMenuOnClick" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - '; - $responsive_container_directives = ' - data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" - data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" - data-wp-effect="effects.core.navigation.initMenu" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" - tabindex="-1" - '; - $responsive_dialog_directives = ' - data-wp-bind--aria-modal="selectors.core.navigation.ariaModal" - data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" - data-wp-bind--role="selectors.core.navigation.roleAttribute" - data-wp-effect="effects.core.navigation.focusFirstElement" - '; - $close_button_directives = ' - data-wp-on--click="actions.core.navigation.closeMenuOnClick" - '; - } - - $responsive_container_markup = sprintf( - ' -
    -
    -
    - -
    - %2$s -
    -
    -
    -
    ', - esc_attr( $modal_unique_id ), - $inner_blocks_html, - $toggle_aria_label_open, - $toggle_aria_label_close, - esc_attr( implode( ' ', $responsive_container_classes ) ), - esc_attr( implode( ' ', $open_button_classes ) ), - esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) ), - $toggle_button_content, - $toggle_close_button_content, - $open_button_directives, - $responsive_container_directives, - $responsive_dialog_directives, - $close_button_directives - ); - - return sprintf( - '', - $wrapper_attributes, - $responsive_container_markup, - $nav_element_directives - ); + return WP_Navigation_Block_Renderer::render( $attributes, $content, $block ); } /** diff --git a/packages/block-library/src/navigation/use-template-part-area-label.js b/packages/block-library/src/navigation/use-template-part-area-label.js index 91838b268b47d6..48763a38ac62d1 100644 --- a/packages/block-library/src/navigation/use-template-part-area-label.js +++ b/packages/block-library/src/navigation/use-template-part-area-label.js @@ -45,14 +45,16 @@ export default function useTemplatePartAreaLabel( clientId ) { 'core/editor' ).__experimentalGetDefaultTemplatePartAreas(); /* eslint-enable @wordpress/data-no-store-string-literals */ - const { getEditedEntityRecord } = select( coreStore ); + const { getCurrentTheme, getEditedEntityRecord } = + select( coreStore ); for ( const templatePartClientId of parentTemplatePartClientIds ) { const templatePartBlock = getBlock( templatePartClientId ); // The 'area' usually isn't stored on the block, but instead // on the entity. - const { theme, slug } = templatePartBlock.attributes; + const { theme = getCurrentTheme()?.stylesheet, slug } = + templatePartBlock.attributes; const templatePartEntityId = createTemplatePartId( theme, slug diff --git a/packages/block-library/src/paragraph/test/__snapshots__/transforms.native.js.snap b/packages/block-library/src/paragraph/test/__snapshots__/transforms.native.js.snap index 22e05ce5435c98..b0855c02bd0e96 100644 --- a/packages/block-library/src/paragraph/test/__snapshots__/transforms.native.js.snap +++ b/packages/block-library/src/paragraph/test/__snapshots__/transforms.native.js.snap @@ -1,5 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Paragraph block transforms to Code block 1`] = ` +" +
    Example text
    +" +`; + exports[`Paragraph block transforms to Columns block 1`] = ` "
    diff --git a/packages/block-library/src/paragraph/test/transforms.native.js b/packages/block-library/src/paragraph/test/transforms.native.js index 0f34038ca298c7..bb2d14a9966252 100644 --- a/packages/block-library/src/paragraph/test/transforms.native.js +++ b/packages/block-library/src/paragraph/test/transforms.native.js @@ -23,6 +23,7 @@ const blockTransforms = [ 'Preformatted', 'Pullquote', 'Verse', + 'Code', ...transformsWithInnerBlocks, ]; diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js index e8068faf5013d9..5fd1b427a5e3e9 100644 --- a/packages/block-library/src/pattern/edit.js +++ b/packages/block-library/src/pattern/edit.js @@ -20,7 +20,8 @@ const PatternEdit = ( { attributes, clientId } ) => { ); const currentThemeStylesheet = useSelect( - ( select ) => select( coreStore ).getCurrentTheme().stylesheet + ( select ) => select( coreStore ).getCurrentTheme()?.stylesheet, + [] ); const { replaceBlocks, __unstableMarkNextChangeAsNotPersistent } = diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php index f05bb333bd186d..436452f6853001 100644 --- a/packages/block-library/src/pattern/index.php +++ b/packages/block-library/src/pattern/index.php @@ -48,7 +48,12 @@ function render_block_core_pattern( $attributes ) { $content = gutenberg_serialize_blocks( $blocks ); } - return do_blocks( $content ); + $content = do_blocks( $content ); + + global $wp_embed; + $content = $wp_embed->autoembed( $content ); + + return $content; } add_action( 'init', 'register_block_core_pattern' ); diff --git a/packages/block-library/src/post-author/edit.js b/packages/block-library/src/post-author/edit.js index 4ee353fdd9bdc0..05797fcc8250a1 100644 --- a/packages/block-library/src/post-author/edit.js +++ b/packages/block-library/src/post-author/edit.js @@ -196,7 +196,6 @@ function PostAuthorEdit( { { ( ! RichText.isEmpty( byline ) || isSelected ) && ( event.preventDefault(), + 'aria-disabled': true, +}; + export default function PostFeaturedImageEdit( { clientId, attributes, @@ -67,9 +72,10 @@ export default function PostFeaturedImageEdit( { postId ); - const { media, postType } = useSelect( + const { media, postType, postPermalink } = useSelect( ( select ) => { - const { getMedia, getPostType } = select( coreStore ); + const { getMedia, getPostType, getEditedEntityRecord } = + select( coreStore ); return { media: featuredImage && @@ -77,10 +83,16 @@ export default function PostFeaturedImageEdit( { context: 'view', } ), postType: postTypeSlug && getPostType( postTypeSlug ), + postPermalink: getEditedEntityRecord( + 'postType', + postTypeSlug, + postId + )?.link, }; }, - [ featuredImage, postTypeSlug ] + [ featuredImage, postTypeSlug, postId ] ); + const mediaUrl = getMediaSourceUrlBySizeSlug( media, sizeSlug ); const imageSizes = useSelect( @@ -197,7 +209,17 @@ export default function PostFeaturedImageEdit( { <> { controls }
    - { placeholder() } + { !! isLink ? ( + + { placeholder() } + + ) : ( + placeholder() + ) } ) }
    - { image } + { /* If the featured image is linked, wrap in an tag to trigger any inherited link element styles */ } + { !! isLink ? ( + + { image } + + ) : ( + image + ) } tag. + // Restore cursor style so it doesn't appear 'clickable'. + > a { + cursor: default; + } + + // When the Post Featured Image block is linked, + // and wrapped with a disabled tag + // ensure that the placeholder items are visible when selected. + &.is-selected .components-placeholder.has-illustration { + .components-button, + .components-placeholder__instructions, + .components-placeholder__label { + opacity: 1; + pointer-events: auto; + } + } } div[data-type="core/post-featured-image"] { diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json index 48804de75d2cae..d2f7c09693121c 100644 --- a/packages/block-library/src/post-template/block.json +++ b/packages/block-library/src/post-template/block.json @@ -10,7 +10,6 @@ "usesContext": [ "queryId", "query", - "queryContext", "displayLayout", "templateSlug", "previewPostType", diff --git a/packages/block-library/src/post-template/edit.js b/packages/block-library/src/post-template/edit.js index f05f81c14082ef..025a8bf4f6c93f 100644 --- a/packages/block-library/src/post-template/edit.js +++ b/packages/block-library/src/post-template/edit.js @@ -96,7 +96,6 @@ export default function PostTemplateEdit( { // REST API or be handled by custom REST filters like `rest_{$this->post_type}_query`. ...restQueryArgs } = {}, - queryContext = [ { page: 1 } ], templateSlug, previewPostType, }, @@ -104,8 +103,6 @@ export default function PostTemplateEdit( { __unstableLayoutClassNames, } ) { const { type: layoutType, columnCount = 3 } = layout || {}; - - const [ { page } ] = queryContext; const [ activeBlockContextId, setActiveBlockContextId ] = useState(); const { posts, blocks } = useSelect( ( select ) => { @@ -126,7 +123,7 @@ export default function PostTemplateEdit( { slug: templateSlug.replace( 'category-', '' ), } ); const query = { - offset: perPage ? perPage * ( page - 1 ) + offset : 0, + offset: perPage ? perPage + offset : 0, order, orderby: orderBy, }; @@ -194,7 +191,6 @@ export default function PostTemplateEdit( { }, [ perPage, - page, offset, order, orderBy, diff --git a/packages/block-library/src/post-terms/edit.js b/packages/block-library/src/post-terms/edit.js index 188a8d22f2f8e2..b5eb0bb9e3bfb6 100644 --- a/packages/block-library/src/post-terms/edit.js +++ b/packages/block-library/src/post-terms/edit.js @@ -97,7 +97,6 @@ export default function PostTermsEdit( { - createBlock( 'core/paragraph', { - ...attributes, - content: attributes.content.replace( /\n/g, '
    ' ), - } ), + createBlock( 'core/paragraph', attributes ), }, { type: 'block', diff --git a/packages/block-library/src/query/edit/query-content.js b/packages/block-library/src/query/edit/query-content.js index 6fef2c43b4affc..4d9b8885cb15ea 100644 --- a/packages/block-library/src/query/edit/query-content.js +++ b/packages/block-library/src/query/edit/query-content.js @@ -131,6 +131,7 @@ export default function QueryContent( { )' ), value: 'div' }, diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 201ceed737a12a..b6a5733632ff44 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -44,7 +44,11 @@ function render_block_core_query( $attributes, $content, $block ) { $block->block_type->supports['interactivity'] = true; // Add a div to announce messages using `aria-live`. - $last_div_position = strripos( $content, '
    ' ); + $html_tag = 'div'; + if ( ! empty( $attributes['tagName'] ) ) { + $html_tag = esc_attr( $attributes['tagName'] ); + } + $last_tag_position = strripos( $content, '' ); $content = substr_replace( $content, '
    ', - $last_div_position, + $last_tag_position, 0 ); } diff --git a/packages/block-library/src/quote/block.json b/packages/block-library/src/quote/block.json index eff4649230a580..d0bb9005f8e63c 100644 --- a/packages/block-library/src/quote/block.json +++ b/packages/block-library/src/quote/block.json @@ -54,6 +54,12 @@ "background": true, "text": true } + }, + "layout": { + "allowEditing": false + }, + "spacing": { + "blockGap": true } }, "styles": [ diff --git a/packages/block-library/src/quote/style.scss b/packages/block-library/src/quote/style.scss index e453e407a08752..8688294bc5594e 100644 --- a/packages/block-library/src/quote/style.scss +++ b/packages/block-library/src/quote/style.scss @@ -18,5 +18,9 @@ font-size: 1.125em; text-align: right; } + + } + > cite { + display: block; } } diff --git a/packages/block-library/src/read-more/style.scss b/packages/block-library/src/read-more/style.scss index f0e0291eb6ffcc..3a15de37adf685 100644 --- a/packages/block-library/src/read-more/style.scss +++ b/packages/block-library/src/read-more/style.scss @@ -1,7 +1,7 @@ .wp-block-read-more { display: block; width: fit-content; - &:not([style*="text-decoration"]) { + &:where(:not([style*="text-decoration"])) { text-decoration: none; &:focus, diff --git a/packages/block-library/src/template-part/edit/advanced-controls.js b/packages/block-library/src/template-part/edit/advanced-controls.js index b879b46638face..8ad4b4bdbeb1da 100644 --- a/packages/block-library/src/template-part/edit/advanced-controls.js +++ b/packages/block-library/src/template-part/edit/advanced-controls.js @@ -95,6 +95,7 @@ export function TemplatePartAdvancedControls( { ) } select( coreStore ).getCurrentTheme()?.stylesheet, + [] + ); + const { slug, theme = currentTheme, tagName, layout = {} } = attributes; const templatePartId = createTemplatePartId( theme, slug ); const hasAlreadyRendered = useHasRecursion( templatePartId ); const [ isTemplatePartSelectionOpen, setIsTemplatePartSelectionOpen ] = @@ -174,17 +177,7 @@ export default function TemplatePartEdit( { } aria-haspopup="dialog" > - { createInterpolateElement( - __( 'Replace ' ), - { - BlockTitle: ( - - ), - } - ) } + { __( 'Replace' ) } ); } } diff --git a/packages/block-library/src/template-part/index.js b/packages/block-library/src/template-part/index.js index c64f093427f95f..c9b5e33a1c9598 100644 --- a/packages/block-library/src/template-part/index.js +++ b/packages/block-library/src/template-part/index.js @@ -32,10 +32,11 @@ export const settings = { return; } - const entity = select( coreDataStore ).getEntityRecord( + const { getCurrentTheme, getEntityRecord } = select( coreDataStore ); + const entity = getEntityRecord( 'postType', 'wp_template_part', - theme + '//' + slug + ( theme || getCurrentTheme()?.stylesheet ) + '//' + slug ); if ( ! entity ) { return; @@ -60,7 +61,7 @@ export const init = () => { const DISALLOWED_PARENTS = [ 'core/post-template', 'core/post-content' ]; addFilter( 'blockEditor.__unstableCanInsertBlockType', - 'removeTemplatePartsFromPostTemplates', + 'core/block-library/removeTemplatePartsFromPostTemplates', ( canInsert, blockType, diff --git a/packages/block-library/src/template-part/index.php b/packages/block-library/src/template-part/index.php index 3ad400906945b8..26d90c51b5529c 100644 --- a/packages/block-library/src/template-part/index.php +++ b/packages/block-library/src/template-part/index.php @@ -43,10 +43,10 @@ function render_block_core_template_part( $attributes ) { if ( $template_part_post ) { // A published post might already exist if this template part was customized elsewhere // or if it's part of a customized template. - $content = $template_part_post->post_content; - $area_terms = get_the_terms( $template_part_post, 'wp_template_part_area' ); - if ( ! is_wp_error( $area_terms ) && false !== $area_terms ) { - $area = $area_terms[0]->name; + $block_template = _build_block_template_result_from_post( $template_part_post ); + $content = $block_template->content; + if ( isset( $block_template->area ) ) { + $area = $block_template->area; } /** * Fires when a block template part is loaded from a template post stored in the database. diff --git a/packages/block-library/src/template-part/variations.js b/packages/block-library/src/template-part/variations.js index 866cf15d56c125..79881ee5f89e4c 100644 --- a/packages/block-library/src/template-part/variations.js +++ b/packages/block-library/src/template-part/variations.js @@ -35,10 +35,12 @@ export function enhanceTemplatePartVariations( settings, name ) { // Find a matching variation from the created template part // by checking the entity's `area` property. if ( ! slug ) return false; - const entity = select( coreDataStore ).getEntityRecord( + const { getCurrentTheme, getEntityRecord } = + select( coreDataStore ); + const entity = getEntityRecord( 'postType', 'wp_template_part', - `${ theme }//${ slug }` + `${ theme || getCurrentTheme()?.stylesheet }//${ slug }` ); if ( entity?.slug ) { diff --git a/packages/blocks/README.md b/packages/blocks/README.md index cbde04f72fd95b..8e6fdc9d900dbb 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -457,7 +457,6 @@ _Parameters_ - _options.plainText_ `[string]`: Plain text version. - _options.mode_ `[string]`: Handle content as blocks or inline content. _ 'AUTO': Decide based on the content passed. _ 'INLINE': Always handle as inline content, and return string. \* 'BLOCKS': Always handle as blocks, and return array of blocks. - _options.tagName_ `[Array]`: The tag into which content will be inserted. -- _options.preserveWhiteSpace_ `[boolean]`: Whether or not to preserve consequent white space. _Returns_ diff --git a/packages/blocks/src/api/raw-handling/paste-handler.js b/packages/blocks/src/api/raw-handling/paste-handler.js index 9fa87462d8a1b2..2f68a826931ab6 100644 --- a/packages/blocks/src/api/raw-handling/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/paste-handler.js @@ -41,12 +41,11 @@ const { console } = window; /** * Filters HTML to only contain phrasing content. * - * @param {string} HTML The HTML to filter. - * @param {boolean} preserveWhiteSpace Whether or not to preserve consequent white space. + * @param {string} HTML The HTML to filter. * * @return {string} HTML only containing phrasing content. */ -function filterInlineHTML( HTML, preserveWhiteSpace ) { +function filterInlineHTML( HTML ) { HTML = deepFilterHTML( HTML, [ headRemover, googleDocsUIDRemover, @@ -58,9 +57,7 @@ function filterInlineHTML( HTML, preserveWhiteSpace ) { inline: true, } ); - if ( ! preserveWhiteSpace ) { - HTML = deepFilterHTML( HTML, [ htmlFormattingRemover, brRemover ] ); - } + HTML = deepFilterHTML( HTML, [ htmlFormattingRemover, brRemover ] ); // Allows us to ask for this information when we get a report. console.log( 'Processed inline HTML:\n\n', HTML ); @@ -71,15 +68,14 @@ function filterInlineHTML( HTML, preserveWhiteSpace ) { /** * Converts an HTML string to known blocks. Strips everything else. * - * @param {Object} options - * @param {string} [options.HTML] The HTML to convert. - * @param {string} [options.plainText] Plain text version. - * @param {string} [options.mode] Handle content as blocks or inline content. - * * 'AUTO': Decide based on the content passed. - * * 'INLINE': Always handle as inline content, and return string. - * * 'BLOCKS': Always handle as blocks, and return array of blocks. - * @param {Array} [options.tagName] The tag into which content will be inserted. - * @param {boolean} [options.preserveWhiteSpace] Whether or not to preserve consequent white space. + * @param {Object} options + * @param {string} [options.HTML] The HTML to convert. + * @param {string} [options.plainText] Plain text version. + * @param {string} [options.mode] Handle content as blocks or inline content. + * * 'AUTO': Decide based on the content passed. + * * 'INLINE': Always handle as inline content, and return string. + * * 'BLOCKS': Always handle as blocks, and return array of blocks. + * @param {Array} [options.tagName] The tag into which content will be inserted. * * @return {Array|string} A list of blocks or a string, depending on `handlerMode`. */ @@ -88,7 +84,6 @@ export function pasteHandler( { plainText = '', mode = 'AUTO', tagName, - preserveWhiteSpace, } ) { // First of all, strip any meta tags. HTML = HTML.replace( /]+>/g, '' ); @@ -167,7 +162,7 @@ export function pasteHandler( { } if ( mode === 'INLINE' ) { - return filterInlineHTML( HTML, preserveWhiteSpace ); + return filterInlineHTML( HTML ); } if ( @@ -175,7 +170,7 @@ export function pasteHandler( { ! hasShortcodes && isInlineContent( HTML, tagName ) ) { - return filterInlineHTML( HTML, preserveWhiteSpace ); + return filterInlineHTML( HTML ); } const phrasingContentSchema = getPhrasingContentSchema( 'paste' ); diff --git a/packages/blocks/src/api/raw-handling/test/paste-handler.js b/packages/blocks/src/api/raw-handling/test/paste-handler.js index e8e1e34c7d57a3..6938ad0d9c4081 100644 --- a/packages/blocks/src/api/raw-handling/test/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/test/paste-handler.js @@ -69,7 +69,6 @@ describe( 'pasteHandler', () => { const [ result ] = pasteHandler( { HTML: tableWithHeaderFooterAndBodyUsingColspan, tagName: 'p', - preserveWhiteSpace: false, } ); expect( console ).toHaveLogged(); @@ -110,7 +109,6 @@ describe( 'pasteHandler', () => { const [ result ] = pasteHandler( { HTML: tableWithHeaderFooterAndBodyUsingRowspan, tagName: 'p', - preserveWhiteSpace: false, } ); expect( console ).toHaveLogged(); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b3b67164e984e5..856926988089a3 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,33 @@ ## Unreleased +### Bug Fix + +- `Toolbar`: Remove CSS rule that prevented focus outline to be visible for toolbar buttons in the `:active` state. ([#56123](https://github.com/WordPress/gutenberg/pull/56123)). + +### Internal + +- Migrate `Divider` from `reakit` to `ariakit` ([#55622](https://github.com/WordPress/gutenberg/pull/55622)) +- Migrate `DisclosureContent` from `reakit` to `ariakit` and TypeScript ([#55639](https://github.com/WordPress/gutenberg/pull/55639)) +- Migrate `RadioGroup` from `reakit` to `ariakit` and TypeScript ([#55580](https://github.com/WordPress/gutenberg/pull/55580)) + +### Experimental + +- `Tabs`: Add `focusable` prop to the `Tabs.TabPanel` sub-component ([#55287](https://github.com/WordPress/gutenberg/pull/55287)) +- `Tabs`: Update sub-components to accept relevant HTML element props ([#55860](https://github.com/WordPress/gutenberg/pull/55860)) +- `DropdownMenuV2`: Fix radio menu item check icon not rendering correctly in some browsers ([#55964](https://github.com/WordPress/gutenberg/pull/55964)) +- `DropdownMenuV2`: prevent default when pressing Escape key to close menu ([#55962](https://github.com/WordPress/gutenberg/pull/55962)) + +### Enhancements + +- `ToggleGroupControl`: Add opt-in prop for 40px default size ([#55789](https://github.com/WordPress/gutenberg/pull/55789)). +- `TextControl`: Add opt-in prop for 40px default size ([#55471](https://github.com/WordPress/gutenberg/pull/55471)). + +### Bug Fix + +- `DropdownMenu`: remove extra vertical space around the toggle button ([#56136](https://github.com/WordPress/gutenberg/pull/56136)). +- Package should not depend on `@ariakit/test`, that package is only needed for testing ([#56091](https://github.com/WordPress/gutenberg/pull/56091)). + ## 25.11.0 (2023-11-02) ### Enhancements diff --git a/packages/components/package.json b/packages/components/package.json index f616b261c8ec4f..2980553f5a2846 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -31,7 +31,6 @@ "types": "build-types", "dependencies": { "@ariakit/react": "^0.3.5", - "@ariakit/test": "^0.3.0", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", diff --git a/packages/components/src/disclosure/index.js b/packages/components/src/disclosure/index.js deleted file mode 100644 index 5458ba053eef6a..00000000000000 --- a/packages/components/src/disclosure/index.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Accessible Disclosure component that controls visibility of a section of - * content. It follows the WAI-ARIA Disclosure Pattern. - * - * @see https://reakit.io/docs/disclosure/ - * - * The plan is to build own API that accounts for future breaking changes - * in Reakit (https://github.com/WordPress/gutenberg/pull/28085). - */ -/* eslint-disable-next-line no-restricted-imports */ -export { DisclosureContent } from 'reakit'; diff --git a/packages/components/src/disclosure/index.tsx b/packages/components/src/disclosure/index.tsx new file mode 100644 index 00000000000000..5bacfcabc349a6 --- /dev/null +++ b/packages/components/src/disclosure/index.tsx @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { DisclosureContentProps } from './types'; +import type { WordPressComponentProps } from '../context'; + +/** + * Accessible Disclosure component that controls visibility of a section of + * content. It follows the WAI-ARIA Disclosure Pattern. + */ +const UnforwardedDisclosureContent = ( + { + visible, + children, + ...props + }: WordPressComponentProps< DisclosureContentProps, 'div', false >, + ref: React.ForwardedRef< any > +) => { + const disclosure = Ariakit.useDisclosureStore( { open: visible } ); + + return ( + + { children } + + ); +}; + +export const DisclosureContent = forwardRef( UnforwardedDisclosureContent ); +export default DisclosureContent; diff --git a/packages/components/src/disclosure/types.tsx b/packages/components/src/disclosure/types.tsx new file mode 100644 index 00000000000000..6a0a746bb6397f --- /dev/null +++ b/packages/components/src/disclosure/types.tsx @@ -0,0 +1,10 @@ +export type DisclosureContentProps = { + /** + * If set to `true` the content will be shown, otherwise it's hidden. + */ + visible?: boolean; + /** + * The content to display within the component. + */ + children: React.ReactNode; +}; diff --git a/packages/components/src/divider/component.tsx b/packages/components/src/divider/component.tsx index 4ac2524456ab75..3870c2f12c4fc3 100644 --- a/packages/components/src/divider/component.tsx +++ b/packages/components/src/divider/component.tsx @@ -2,7 +2,7 @@ * External dependencies */ // eslint-disable-next-line no-restricted-imports -import { Separator } from 'reakit'; +import * as Ariakit from '@ariakit/react'; import type { ForwardedRef } from 'react'; /** @@ -20,8 +20,8 @@ function UnconnectedDivider( const contextProps = useContextSystem( props, 'Divider' ); return ( - } { ...contextProps } ref={ forwardedRef } /> diff --git a/packages/components/src/divider/stories/index.story.tsx b/packages/components/src/divider/stories/index.story.tsx index d60a43164506b3..4910c1b591c524 100644 --- a/packages/components/src/divider/stories/index.story.tsx +++ b/packages/components/src/divider/stories/index.story.tsx @@ -23,6 +23,14 @@ const meta: Meta< typeof Divider > = { marginEnd: { control: { type: 'text' }, }, + wrapElement: { + control: { type: null }, + }, + ref: { + table: { + disable: true, + }, + }, }, parameters: { controls: { expanded: true }, diff --git a/packages/components/src/divider/types.ts b/packages/components/src/divider/types.ts index 03caedb6a5b3e7..cb29823eb4ca1a 100644 --- a/packages/components/src/divider/types.ts +++ b/packages/components/src/divider/types.ts @@ -2,7 +2,7 @@ * External dependencies */ // eslint-disable-next-line no-restricted-imports -import type { SeparatorProps } from 'reakit'; +import type { SeparatorProps } from '@ariakit/react'; /** * Internal dependencies @@ -11,7 +11,7 @@ import type { SpaceInput } from '../utils/space'; export type DividerProps = Omit< SeparatorProps, - 'children' | 'unstable_system' | 'orientation' + 'children' | 'unstable_system' | 'orientation' | 'as' | 'render' > & { /** * Adjusts all margins on the inline dimension. diff --git a/packages/components/src/dropdown-menu-v2-ariakit/README.md b/packages/components/src/dropdown-menu-v2-ariakit/README.md index 89fccb54e7d011..f74098efff4103 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/README.md +++ b/packages/components/src/dropdown-menu-v2-ariakit/README.md @@ -113,13 +113,6 @@ The skidding of the popover along the anchor element. Can be set to negative val - Required: no - Default: `0` for root-level menus, `-8` for nested menus -##### `hideOnEscape`: `boolean | ( ( event: KeyboardEvent | React.KeyboardEvent< Element > ) => boolean )` - -Determines whether the menu popover will be hidden when the user presses the Escape key. - -- Required: no -- Default: `true` - ### `DropdownMenuItem` Used to render a menu item. diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index 5d0cd6142b4bea..10b93d8c552c13 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -14,6 +14,7 @@ import { useMemo, cloneElement, isValidElement, + useCallback, } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; import { check, chevronRightSmall } from '@wordpress/icons'; @@ -99,6 +100,12 @@ export const DropdownMenuCheckboxItem = forwardRef< ); } ); +const radioCheck = ( + + + +); + export const DropdownMenuRadioItem = forwardRef< HTMLDivElement, WordPressComponentProps< DropdownMenuRadioItemProps, 'div', false > @@ -119,14 +126,7 @@ export const DropdownMenuRadioItem = forwardRef< store={ dropdownMenuContext?.store } render={ } > - - - + { children } { suffix } @@ -181,7 +181,6 @@ const UnconnectedDropdownMenu = ( children, shift, modal = true, - hideOnEscape = true, // From internal components context variant, @@ -249,6 +248,28 @@ const UnconnectedDropdownMenu = ( ); } + const hideOnEscape = useCallback( + ( event: React.KeyboardEvent< Element > ) => { + // Pressing Escape can cause unexpected consequences (ie. exiting + // full screen mode on MacOs, close parent modals...). + event.preventDefault(); + // Returning `true` causes the menu to hide. + return true; + }, + [] + ); + + const wrapperProps = useMemo( + () => ( { + dir: computedDirection, + style: { + direction: + computedDirection as React.CSSProperties[ 'direction' ], + }, + } ), + [ computedDirection ] + ); + return ( <> { /* Menu trigger */ } @@ -281,12 +302,7 @@ const UnconnectedDropdownMenu = ( hideOnHoverOutside={ false } data-side={ appliedPlacementSide } variant={ variant } - wrapperProps={ { - dir: computedDirection, - style: { - direction: computedDirection, - }, - } } + wrapperProps={ wrapperProps } hideOnEscape={ hideOnEscape } unmountOnHide > diff --git a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx index 901a1c32ec4743..a6319c6cfdc932 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/stories/index.story.tsx @@ -40,6 +40,7 @@ const meta: Meta< typeof DropdownMenu > = { }, argTypes: { children: { control: { type: null } }, + trigger: { control: { type: null } }, }, parameters: { actions: { argTypesRegex: '^on.*' }, @@ -494,10 +495,6 @@ export const InsideModal: StoryFn< typeof DropdownMenu > = ( props ) => { }; InsideModal.args = { ...Default.args, - hideOnEscape: ( e ) => { - e.stopPropagation(); - return true; - }, }; InsideModal.parameters = { docs: { diff --git a/packages/components/src/dropdown-menu-v2-ariakit/test/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/test/index.tsx index 3976159a82e68d..297bc0dec9390f 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/test/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/test/index.tsx @@ -197,32 +197,6 @@ describe( 'DropdownMenu', () => { ); } ); - it( 'should not close when pressing the escape key if the `hideOnEscape` prop is set to `false`', async () => { - render( - Open dropdown } - hideOnEscape={ false } - > - Dropdown menu item - - ); - - const trigger = screen.getByRole( 'button', { - name: 'Open dropdown', - } ); - - await click( trigger ); - - // Focuses menu on mouse click, focuses first item on keyboard press - // Can be changed with a custom useEffect - expect( screen.getByRole( 'menu' ) ).toHaveFocus(); - - // Pressing esc will close the menu and move focus to the toggle - await press.Escape(); - - expect( screen.getByRole( 'menu' ) ).toHaveFocus(); - } ); - it( 'should close when clicking outside of the content', async () => { render(
    yd}4>>#H7@7>_XMTWoxS3mLigFPpCm~JhbZ&TQR*Hs*76= zN2YHl>cEV_l~zU*o)=-JrxLdHy)G0vI+b8deqQjJ3j#>f@cF)~|9o_8bkXpk%a6zL zCv0K4el7giupxDnDW+vd;>nSlQP~&jg#*+_MH39R=EC>Uye`C$$>+h{<$Oz+AgXoy zB!SgZ-puU1o|tlD9+UAJ_5F528=*B}oTGOvp1JZecK5 ztL=7=#X_jSIAoYfel6UD%=HdPRTknCJ6&%a<~i^QO9-Vk}wFCmBmO5Xd41;$ih$JZ=aw)MJwtIF1I~{ z!97=fdM=A|G>%x@^VFwr!s3>f42IU3FI_?@)I- zl~Rv`q&;^m2zBgI1@-U4i+HcV!`O}F2uj$JM9+7ih!UHjq<{@Mzkj%rtjcnZT^G18 zNp&UL`5~ip`6HbyzGfwCFv0PsO3}Ll0UReS7qYDRbQW=#8Sl$V-! zoaIcQSkrypnKsvyRMjzWCMyTB|6%jxEy*m#)aBi^x%a6P0f3RhF1W?+C+LDFE!bu_ zeWHT7T96wGravAim~q|Ub zlh*709UnJd`H5EM)0x$lg1NAP{hf;@yd;q%9jJ< zwNh2kHODk9((E#$Qh=nv&S*$JKd^eEEm@?B+OJxx1#g=~cb&c+r+vu=db8TDg1d z_wE5W<)3^edw+5N!Pcl{j&{JJ;t1$>ht;i=ObF8sd} z_I`7+V8Agh4|+CRh!e?CBq@k$2Fu?RND>8TG$3l}?y_Q#>sbAgh8$v9ZaT|#Vn;OL zeqHKzmWT9TNm`XRA;c#@*Z(@+Fh=?h&+m^P^LSH#^m6gJ{)9dRTW|d^A?k!0Z=xX! zz$Lm`nj5VgOwX-KNv)TX2oXKKM*5GOYRq4K()HPL)tmmev;Jd@r0lOhef&}VUVHjF z(tl;q`YnjtbsRFyTF5M>`HY#Jlv&SW-d`)Y^i1ac(z9KOY>HPgD?>=_^PQH~(IvgH8& zJJQZ?PL+WOB_)y;U+*RNs%EWs_m4PwT28+KGtOex1CIugLTD_UulH7XUM?5XuC791 zZ_-}Qt zseHz;Z?BR{z!`Cu_FZR+WkoNI#Bm^rQPzv%$Z%u1HF6ctOpl4IG96Lg1!~xh2+F_J zWk0@6O{oUrH*d=2A+yL(-+1xO1jH{Nxv?qk96@eYDS7vR3RB)g)~6bHrANw-Q+(|2mu_h3auvgKp)BoRhmlt1-`G`$t~C)XTujio1OUXI8iyL1e zixi}D{w;fb4GY^CTDiFvuJkzB9;q&tb{0fz!3PVbrZwgw@0Cndi7nhq1ZXTr}llIeVNCGNkJXRS`N#=_uSK2e*R8~fy%X&JWIpuYrFjZ{I zSpbiiObffd%U)WSON4b4UvKWug=4gDcVcrKOpr6WeY|dtfj&xlww4HSg2@5Bb)vHA z>5_T6(tTRz7RrZkKncZ`ZLPGlEctyBGb}j-i$J4KugF@F8HF{EcZKKilgss#vhn-y*o%e;1}OjS)EkQipFFOR+C^x7!2r5mKplBmac+xW*p5W*dHb#+cuviwABy64z zB-ad|4-kvP42DS~2Fx1yYiJX3N%p61!l}8Vff0ig9FRq%L%1>?KvX+W(bH0}5TWb? z1UPcS1IiWgCMRAm2P~En#g}jkhsI;l1tDThPC{g_<0Mnixg>tTW|&t_uUyg0D^kog zN$uB@59*W8kq(}PNVP)P6?QCB%fu)wcy>A+cpE`&CP`YB{aGKh3|Sd+>Fy2&3rnGG z*+!;UOxdp8l}9)N#XGVfk#;M`_1>^t+tXO*DpYg{NsS7vG2c04%F5wOs6-mAJS1As zkNX+Mfz1%jvOTb^C}}cheKny#{@$)aVSVh73hU6AV;Kk7932I%(PK=q_VZqol>In7rk|Ii}HK!RTM3NMmV(G%1bD>!QRr0$`gxX2f=o_?_}=< zibSNLibX1_;pUHTgzS7n4Mf-;QMzZ2Pd_~oy%=?Lo5ulEL$TZM=uJW=cy5h8WJ+{4Qu|X({yqD!%G3b-JGh31#uL`Bwf=k ztOjf~9$_b3O%`(T-{755=`eV8IfXTh-_$9#DzsFA<)to1I=MZvK$j0u6|X7BIpPk{ zU9i|}YN0X5Y_DD{_UD>fVBu1bB%X4d%CykQA2QlY}muPz)42Nq-+g_R9p zlTixej*xU@HC!=9&o@aGWJw?s_@TX`0y(wd*7Bp2*ZeW{L>#N2eUg6CM{+;LRBfm8 zu0m&Co=(Ha3u(t^b-C|lLbdk=ff>vKShM;OROy7hnYlN?sf!XHql)POAxo4Tt#L_Q z5g<5U3X-roBvt8na_GY|lXIB_ddlTa>XL0j@ad8h{ZvsxCE5qR#QDaTOqMkgwz>Rr z-F>j36e(2kQ$YIWF=_GV;C(8IpQItiz0NzFy~u^os;mJFU_52q(O)sWjF>aaWEA%6 z&-&%-&_mvHD;2@^)mgCkF|}oI(Z9y^{R!zwbwCxaz=7= z%#z&6#DA0MUpBnlOeh^DjC(5uB1Q(=j#k}9++{>hv6fkm&}rI{uwp+9E?X3nNF9gT0S-i8hEk-d86a0)u-*$W2erYw~5PkcjgRzc^ zLl=`FwWxngX=I&Gq98N}g;k4+NZvR1#6ma^5dbMUlxUNdz~DSWH$*xLE_#e=-zan$ zNIP<1_BP}EEZlHs3PsRqgqZeR?EpAxbtcRxkhNr{DXE}}OD$zWMc?!`Xhi*>xgsa6 zbIlSt<6JwxRFoH!rKc*KFM0XMv2ed+%gf*@_|jfp6QW!yZPwKIs0DKhead#mWoyi2 z(`{1KhE=E(Esp+t{a7X^$&rFpYtH#Zo<#J*+>ABa{uC#ftf`B;cPiC5Et+u=NK@4B zkRw>+jW!_SQSWUCmQfs!aJcjDrYqWp&{-(4-QY*NBYQ{+l@USdi51gt@}ur*f_<&9 znIqVHL6BZxb2>p(PB0~lZ79%Js(?8yF%VKAs=%7k^dc@yY7Q3ogbOo1%mWu@I^ES_ z@6cnB^}OJ*YOoiqAXXU>Pa%862$U+vF{}jf;wsymedD{!Hmi<wL@FY>`sm+~4jLd72iFT+kL& z%TqSt7+WtI8($dOxxRexJvr-EH17TgjMV+-p|OU`Y@-NAUfdAqSLKAK(!|h+avd!- zd8TyicmkYJMpr2{`Z&jDVG0qt)+?@ehpRl7$iGOq0G%tKK)uelFPuy{HtGu}^UmnB z3(*eZU1)axH+)zGQgj}24y!7LStGB_E;fhm8bu&W z7>446Qr6;efu|f#(6T?+2Xc^e9HTYe>Jv*zGfy^)`v`YFs5p!UCFl{Inh3!lS%i~VB?4VMbukQ?f{g=Y$-4OvXAR2fY`&SqNl)2 zAkbE21a&NW%GOTTxt7@zfe|eo#{w6lCI44S> zp#Vp?4+v5~0a9)rm$^)as3<%7x!<&9W33ZO6p8#7C)4+t%gWFcP z=5l6RfR4h}t~lSKV{yY?Ts&5W0)ChBno3YuT*}B1F+a384l{=HE zl%5iWvZCLo-!qvyst~?xV^7S6;qjb$~V%bL{biwDIT2A zNmr})m8lzR9eMYJGSv=ylqt5IY#qv!6f!Z!I0TeAM495{;pg2~rY!cBh<%cXsy%#Y z=rWp&A0<=$5LXM8AJWjim@UDaUGMt*6LR*WL=!^uoJMJRU?Vk0fMyqaiPt5)U>rr> zKTa5&j5A%7D=xb*JURh#>l|;Qw-;5wP9eAOlwrR(>0$A+u#1^uVkv_zP5y<7pVt@p zRmIOqG!D^OIlrD${G4ahT>W^44#?WKGY7sZe63<58@{Ug)4s2Yh)l{}IH(dAm6GV9 zM-LOQERGcZj9RJUr5OtBg@Xl8g-Jg1qq-v_ea%$ zw^D5K>f&s!POhR?cV^DNo`1!$nitDaXe#ItBdi;|g@W6BWbK~3H#apEbWJOnePo}4 zLc@i|z!e(dBxAHc6Q`=O{9|P4rEg zrvg1%XgU;$u}q_bqd%pEdLBVhjFohwBN@7BeXY zEF4M!c+mBthNgRImM&K8s697X@s6g3a%|!aje~`0-b$0hyRb?DOp;#KZt0;F z$Kgxnb3RZ*xn3TdJ7!fwjfK+|U(6iaWAme)>qmRAAF>I`3a>|(7Ai3#=RDSatbjro z?ACq-hCDd&&ahTDXSNAbWVX?5i^UHGvu^O3c1PaFu596mx+zo1UB%)PlP01Hn?x4E zr%4yROX}4ldFn{IG>MeStkT6@gL+NL-PGYQk^_V}qGt=umSLYTryR@Dvw7a4 zGkym)i4Ug}tY;VP+Kgr|!2{Gwo zhwlr-r0M!k#iXC7Y!8R8%8{bjH6-0+p!mIP!sy!asF|R2?R&9lGBF{A)Wfj(THTxQ zlf0y)QH!U_kHx6Pq$R8Hk(9Sr>lbpEYQmdRsp0YW$|9(UU>_Yk{V7d*3Q0dt)9#gT zCmg;#B>m+MU)l4|4@h@Sdw2(y0Ae|O6QPX5S1w?I9`~qa)3j+CW2H9ObA^bX>(yLB z#HX~nIyo=m*35$Kg^1-eB~nl!;-S?E7RNB(ixC@h-M(7)T(_n=2>~){om|z52unDm z=*6w6)57&rZq0pmzGajoMl4xF+H#Ca!FdH+EyY0WIfTw7py(jl-Z(f(FcmtP(zSp1 z{zdhfQ?QB2(O$5flVYaAEzOii!kCZx>=d&Nj9l>2pq;sx>4{3JRCP2+D}l2K9c7HI zF2EH~`6VE0Z%)FlN&KvYy7*R8$4j+-90Bdr-l^CZ)|0$CB3KI%zFhi*h}Y~!SVmF{6TpR5>O*#8=g~E) z@G#*$H;TDiL5cC6ey@6|;0riKjd6;qp={At-YY}pNaFujz>o>X%{j)(4;?KzA&cCdg zt%6(mkfl))WX64MvNVb+`E#=p+*FmSvbb3Kkc^MMI9}4$rZ3G{nOd0h%ZITnwJM6a z&gA#iqGU~E32Qpvm!yxRxVSIKSdSErw*?vN@pdf8SdXMDwFRk?bKzU3Tf9C?a#A@? z+ZWzJ`!c}+5ZhHOuY@64P>RmihC#BQ`FAz%SdYw%AAlZ+JmoJ{M0ti(uf2#Mv3QnFUDC!`a%lE-Kkk|ax2C~i$oT^~F3 znRcBwTpzLNGYexOncE@1TjW=YD(fzpLD5rHj#(NJ(Ef&p#8R##;uMtACBH8?XUV)R zS7s;2a~kn=vO;A_;y6@VAEG*DUK^gfRGu=N{MUPwUuSL{A@0^apT(eV7*k3@!%%*-NW>%_3wedt>Th(X#A?Md7B>Y zzg)P4$jxG&Tf4{L9)er(CK-1!4>|}!4tI?8>fyqKs166yQ2(2E_ghxng!&`0J^=kT zvWc1yqdXy$-Fkg{cW#jLK3-543jaHF{r%IF#@MWYJg7){sXD9O$vP`?ODRK3pONST z#p!X_bnK{9pd^;orYI`|$IRfBpY{`L92G zcXsZ73QODneRuZr4}bXX-yi?`Ki(^d`R?ET_218a`uXwk58wUymp}jLp96cR!pVwn zf0qXL>zR2yGk^W(zh3?GFQ0s7PUc4n{nM_3305}dJ$Tp0zpareDT(U&l#v;8D)mx- z3nNp^C2b_W+{%mv9)qIA@nd8f`3g2>l*=+uqzzy18<`n7VPwk3Iomff889sBZgd|S znQLbaM*PsNUzyY^lln|1bvn63eTpb4$^MU(T=M&GF!0JTUOC2Ra*R{q+Zk?yvrJ)K zJT2+1tk_cV+VVqjePAE%g1-fiRThVOtY8juDcwpqP@%sJaB|$;%B7us1*<7PoCU*v zAE1fx#>)>W9N)OfMR&u;7FZa54G+GPUKtN{y?bk#d|i@EdETM1g*w?6Geshmlx{wphk-#D2o*efmb2rxIcBB?YQ04#*+lG+M@!< z(w>!O5ba?JG_|LVNlw!hLsK54(#U7sAm`xTwROvKr43cBTj0Dw52$?`3zGE!40WAr z0&TlLStUoijir4&m_sfbySgD2p+X>M-P}Vhd`GvO-;aapCyuskQ*nXJGcL{Aq&GFs zc+8I%bCW!m6_mf$7PTUEwSWk5S=43cLr3F--l;VSNJ;7qfLa9MP4l~rxz-*fL!!T+ zZ%`n7YR~qkT1&Y#)og_*A12y&hmF`jyXZChb2!(7CBOXP@~0Pbv!2SNxwHu-38>N! zj*7681TFjo1i$!Gf;6`fBCZ|;5`@A4LCTUB?7cmMEx|BXrRaVkG&Q24Lg3p$boG(< zi7Z~iX9@%(p$Bj8`hE6BA?@AnL*iZMJm_ns`^NOsQAz@4Km=Mcl+?!-fvnLA9+CAx z`&8kj_&YdTX&w_;QR$oY$=49i@}qJy`%sWH)!hZC?My&0vK6~cM6f_O6A*-{6J!_P z6U=Z4o-BBQ@ObhQ#Z62waVPZy$ukfD1MDN9H;utp6c|Zs4ERAwXbjGu@LnLyBU!}& zxrA|K4>kjij{a*5;mFJN6}rs)3hT7r+iq!N{pJ<`taUlXVJ6x*T3mazE-pN{%s<|X z%)g2qRdW)qX{CAlMi@@)8J`)e(Lembst7Qp7krh!;)#;1 zXney8zy1=*Z@W{;T{bz2SSmA;{02wo?z)9Wo;aTCc`iu0ToqdO-)Edx$1q&~@8-NJ z7W|a+ig-58>mErx=e$bE94GiI9@o^V9Uj*<)~WWyaaEghInL&E_WIE~KCU^}sDC-? z(+MAEy_5PrgwfUaf2<(jh<3E&TBPx;=3n{7 zzy0e=i22)78JKb(B&ShAe#egI4ep`jRGwlXm;P@3?HivxDZu}GndY`}^!K7{ePi3` zD>Kcqo1dj@{myNp+f!MNVB?at+}n)Nux{^|j7q@*Rd!xBT`+_5 zE2*&2t81u~=$-;wf6j$XiPmYW-O+~nEzzNu*UcHqV{oxI)Wqu6Gtyvl>e>6{9!T}w z^6!AV+{h6piupPCVXFuaQl2cnmVuku#<>qib;r67BLReez1AkKby|D+JMh>P?}eml znWni;`MdM1DYEj|7w~HKCkKmmd3-hnpF484rP70qzmwekp`7Krs{l>M*HG(tu z+F$md=fu;nx9TagEz4yrdX3b6-Sg$t+gpc=N;2M9T)j#(y=cqamNz30BPZDs*I$#0 z%%LN!g1N%lx7CDoq<7fC1~6mWGOqdpKua%THA) z@Vxe5nKV97`MZv-YrIn1>YO;g{ORg)uk8ibtI&jvZcYOba%ltAvnZotd}co7D$8C( zLpqfrsxgFuqJpS^&0Bc&GH@+m5YlsE;t13COb)3+PXVN~OBh4_2h!}fRBNbQRv47I zF|AA56Z(&besNBuKL$(NuJz}yL*oV+xcwf1r{%)3`s>MUO6@WIo@polc=hAH?R_OZ zNijoG4Zr4Bb1;7Ft7Y+|W~NW_;Hzt$$MRa$XMFYkJtx1q7(0yig=0Zh(7fY|8G9Te zDZ>ccf11ODzkda;9(MCB_`2eUX_fx|`1K-x8dwhF7!9x>@UJ? zPh~i3niROjgj8`z5=n}JUD1U}rzENqStl`1)lJ$~sCi1RBYv~XMB_JUq6OQUm{Vs4q7nE)A9@eqEd3a+)2Xuy!S{k7%cz?plE?!r{+R)CE`|i5R5rA-n?9`mT1{B(mo1wbJ~?3d{jo;N2PsQ|EjwE;Jeyb z|FZEtt5_aNAAm6n9m)6>s;0@@B?wk)*j`5mXf=5jF=!!LoJn|yKAC8mvFwXtHlgu+ zf~F!5ux)*2n(X-@3Z-3|EpHLKG-razs=4AA>Qu09fK{vp=e2)QCaP^O^_2p)tk=45 zV`y{b)=wWrR^!tzF?e{``g*ia2W64I&%`ptD%i{s%>9x$UStHMH}g8!ex2Xne+Wby z=)0@L&&Cr}3@kTMHS!==(kA*N$Jm1v2-rflH0MMTNAeb|aUeI+j4S@n(RjR|UTlBX4}tzbYT>%}^eUCEAF$3((Bkl0!mI!9HcP)iQq6;q<=?;%)_8mT+X z&l~!Td9JR%i^i- ztj0Unp>fChnVp`p7IfZ(Ta$}X4fGJC#v&TAY%G++W;Y~}M08d(xG;0v5&$T4)3?JX zAMeqiy^WNZD;vZz^N1(7$XKk2x!A4aFG#zygAW)Ry`IGSkZ}}?`w9R$i@HEor^3dO z>obZaSBU90ZDZt%B@d+xW!v;7gyor@!htgOM;8-d(*TFlq#%P{-X-{Vu2T*R9 z95zWT7SfZ`uQWYpZ4y*oz2J}OdV{Rjk3D~LC)Up#I|G&CxCAmE$0ai~7a6=1r(LFo z!FEk5tS6-E+reW^r^+f&67t)^0zTvP47D9^1bB!dsS7FCYI z#wjyzj6G-|#@3L5kORr^EoKzKAis;t!rCg64+N=GLUK!0j7Ng6AYc!E0US*SdUud( z5zP!I!Vw+WdBj z-Jz{b7FOpbDMQ>1yX3}ruMD~X`pdx9)yX>WHR!6F2B(rZs4aR-CTTC7bkcm0NH-}E zC}}=yh`%5Wzb?nn6J@DMQ)+diYwBqGbyzssFXW3@YFZCVP2pA~Vk`f~Sv)_BmxXJs z`k?{FHPO9ENi?~B0E^H5!&XwCO078aS z&v0twuheag`W%;^Jx^R*?`9W-U?Xkt zGRo6BkcVT;S`rbxOA$I{40J_dfCeo;ccHgv0^>RTws%gYG@-QGULj`3lj2YS#_%!B z>d`mlc`$l_6{uTw;GR}TJKFi;K2Vs|^q?s0DEQXaeFR1ikbYqm2gf&^-Kqs6f&(P1 z+pa_|#GlS?w^xtVAx&G()*(&WVut+bxI>!c(zJ(5|B-h$+809rYanMH`RQz6`{E=jUjg<&YfU0o4g&m2pXd%+oW3Fs1bro)=x`z3hFWJtyyG&H(U2i| zQQ9Ehf)+08s)in3q%q=2#CLgj@@ugtRO&?09C%iNv(YInR<%V7OVz<4aJBS28BGlk zJYR9Hg&guhz~Q`s%f5z_=y9~d5R@!F9c~}9Xn0TVtHvpXPAxB6rk#soeF6swn0CcuLjnd| zKD!9H#e}kVX{Q|~&on4;AuLv>IY-tE(Wex@=;0Kl^Z;_Z?@A^-X^{u!EAUh{v=Js* zB?O{1uIM~j51vTdhH}R~?&B@Yt z?!Gejg(Kpi%%p&lH-;E~DCRN#y~k@;F6cS<@&Oa}wW#0!p*{kl7G1*ix%0+2ntFI;+2w{_M=$#LR zG=MHC2U&+!-V`a74@YXwy_43s8c4-LY_dN5M>&sf7AdL&5LW1;=8Dd4N4$u$>txj* zgNgjgF(mcEe69;h!(w4165F{Bd{*fjXaTKWrcr$TAcvP%=!z~KaYE-TgG3fl+WXU6=)-LRj54wNS~e5?j_b%p0T)x zCg1Z1C$@~4gfU}E#|K7;<7cppC=fUrBnfOd55mL)Y>LEb6nuNzEW=c zITYR~|IVVy6i37HiZw$!qYSaI_Q$YeN(`@Mn+KJLIu~yRQys%MAsqJU8GAW4SIssE zBI*(m611O$3`4F*h=_d zaEcWMI6jV6;9Db7bo0;)&eR3?YMzXH^-9rf7~#b^3M{OTlwzTIZ^IZ$DOb^PT669e zVbiXUt(Hf8NUIPA$o2gc@2>tKH8|$If$2BRyGnPkL1_l&O-^}j18YVGrb+(MCagKN zr`0Qj=e3#iAydz}BjBPF=}AK~rtOm9EYSGt#vM_f>M0-Aead2~cY;^9ES5dpU#oHZ z^5qYn(pM@ArI$-~U|Feg^;oDu!pLG;mL6n&5$-%!SpL(ZcFcGyTnNriR6Ft=gxYm{ z*{RPl_@TLgcqiVEq2{sT`+7O|&0J^mPYcydQ>JB=W=kiV=B(dx8(k(H(7SbPnYFW|2b_{d)}6FGunIgIEbgPv`E1Q=lT;xDS#FdA!HO z8Ae5Pda?I{V$`GYEI*1>_mTH~G9Hqn_>xo!q;T-Pe+^XLeMI9H-Lu~w-9-e0VY zJhfVD#6q|m&F!D!%};oO$6)t6ZK&Ka9z|U&q#2zvJy?DV6u>@OH@+%!oe0c6*Jjw~vW! z57^OoA6n;nm}SfJc4COg5$X>bRc zMw3te$8@BchVnOF;l#so3x9(!kjrR$SLx{rq?W(u^=(G&DShn4j?kV3xM7$ut(Q!S;+^OBcMJ2RALA7G=^ea}rVVDO z@z{**@hoUJPNPhO&V<(og<&2(sm7jolwfzF_}ZH+{QQ$2^&*rx8>NR(t4U=SpDC9G zSBH?<{mrtB^`+uL+-kw|alFwTXAwRvb&-E9+!%;_f^xCw;2>0LB3<4MX$H&-CpFOz z(N^hLx?@490CADaX?86`t1cfy$&RSkpxE*JGXLtG{9~e5c>x<|dCKG43ddGbP5;FS zL?060q5b3se-K}g2sW2EFFgm5H~?RvFr|r3%#IrT}`Fyk??}Q; zJ!N^9SD5GWR)=T#s{`e==qrA=G1lKJUC07o-o2al0XRN_7&T5A8c<+pCSlnCMFy!)pg!B%wzFSdIZMcC;n|C23pNmNR=dwr;H9Szkhtvc3`h zJ!q#tKS7*&<$8TxE)d#jMN}ez0$EGT_lKv}N`!uC*7e;YtBvGB=LT!>eE2$eN`%P= zQ!$cYB||pR#pGji9B1qUcqLdG_m}Vcz}r6Z4q6MBib&tyjcR2icj#Tb-ntHro5y@M zWc@^L(}ae4wLW(pI)3ho127D|^*cFu5#Je{`oI|`0o90zx~Jm`ImqB>t9cS z;XMh9eZS|8J3oK;@%EwZo5PW4gA>u_4cnn|M1&; z^BDt=KmYm1hwuO658wUqKY#meZ}k8E^0S%$>F3{m{B?}izaD?~AI~@+!#{lgm-pZQ z`s?@q{fF=N#(w$fr@#E@Qdj=;%b$OH`_uP-`uWjCC1cvqUw{1h-`~4r8v4u6|1+lj z+mHYC+uQH|{P6ylU;8zG`R%{+oqziIzux}w@y9>^@b*u?{P5%dIs5Z3zy9g_$9Ugn z&j@vI2}6i`hziSfMsCaCWG(ce2Qh_&_C<2bjyaQ>q+esgPW*Aw?rzqR{7HWB?T3Ez z%-hD;5}FN{lSO$gh$~%yowI`j6*YjWFH61*^!{@i9lDdR`WMjY|Msu`@9ZV$YR_@` z!`;;%fshpX`YT;+(3Q}KJeFpc(=t_0kf)&?iAM$G(22udcEZsFy%7w7wEPNH^t*zp zR1~)Uxo9mUwho0rWekh!pRa%IcdjGT3;GR2Qh!1JD}|#^ssDH<^|29T-btRLQVAxV zbiH*Y*cdm zYsackUdG?&sjaj8cBg9gaxW}SLxa~N)sEEA3b5F;kP4kgpTQ%$p9@qyTdfm|?Jf{9 zNv@(Ga;Y4Jg4Zf0KSgdk`(0UlA=zDc66z9#y+LVt9Rf~ac)#9ploKU4>@xYaqHM|E zO6CB}yNlFLF$NQ(r=y8G-6T>#Zw7!!V{Jd>bUlO*(4p(jdv=e^z~!mlnEWm1tkQPuWE&Ub%L1BxK%m!E`UGmEt!@tTRhRi;?z57cD86 zWGtF9dFa;|dS7a?q`{;=kxt2(VCm?xA^Jhbyh`2xABCfqgr+{y8OMioa*Mn0;(Z@_ z;4h*yCzCXY^YurQwP13hwA&d&{g)|}Dh-Hb^0KcvG;$jBo*Sq4Mdjffb9p_vhf3Bs zxmWgBhG$WsTr0Saypm;2R*H{g9(it3-+~1Jd-C>PPPXJkE~?}_mWQA&#Hc&zu7gxH zxqk9VR=R$_j6ATx(2>j>#O0YxiImSdzDe_OxaYy~cU0BqbLwQtM`|&UvGt z2!JJDYK=~_Nj?g#@Gj|UpZt02b3XfP`kIZe`Ibjyj#9lIktdHxgqv*CDlOqL-SIqL zC4DJ*p8-CTMS~wE6BxUV6G=sb14Ew)Bb8CT{M)gLScuxWQqKD^23n))WO5$-B4P-j zr2Hl+qCZK#^R2|zN|WQ*OFicOy9g3l%<4xLNdoD6I>QyziCi*M?EL)Z1Nc-iB8{yc zRC=b#y@R(jYH9Ugk=o8;vW8r>6p6J#S;{6S1$rXc-5<}-_g$_@vL~=S*DgT8NycQA~vraMOw>mUO5#Lw$%H*~i8s%ix z;PUX6G3|CJnl0G(bEc#9&@}I!YTIr=E+IPmUcZK^u^yw{qSupxH;bbDx{tiQj?UC+W`cXfiJl`t?HCbO9jk^x|Kex!Dj|E(-jK>d1TfORa()t}evQiK*m4JajkfvHNADlf^*OV$v zN^(#>^22+DG!=^PZf1d+F@=p+54skl7Z5etu>)L|G{fXmr9h5Grv=G6IDST0WKO2u z@nS9|4h=fqI(alv$t_6}n2g5tvvcy&gMD6!J~p1ab@JBdPW!Vyw|>=5tYho;Oi%uH zCvQ&7cs+Sfo;-bBC`B@_y4$nmX^n%@U34?W^s#AQD+D;}UCo7``rV=kwViRv#C8OE zF!>0Z?O0>004-~f&n|)M!`l<5n$KAGQf_hjEq!qP-ucPndk6i_A3O%9FYY?qfzD8K zPDdZv@>jyja;Z{&St&Y;{NMcK>2>36?ZW!tb#10891Fr@uKPKih<2$!#dP87N?dOl#I<#DA_G`Dl^(*u zsnD-0#ie~IDT`xdcQcW)P|7kmp1fsAtxujl?lZ{t=uA$_1}K1UMy|)U6E(U<)@NOd z=`+Su-ZzybE52ShecJJfTt97^zkyuBIp#(c{ z);@*Nq)e+CNjyzTT(jk1u~8pm%aOYh5&Z&IBldS-%hljY91j8xft3)G=0t_61>GYB zq=H}&V^%Y;SyAzDl!@iXM8qwwC4@+sTxR#K9ET%O~Z zUDXoQ*h==z8O+BSm+o|XWY&TGU8knT*4wA^c%+uhAAIoSPctFE_le&^LZY45^LQfD zYE{43aY1TvGRhqnu(SCGpc@Ug|)#@2hNr=bh{6bEoI7GIji#S8EK84?aKtcIWyv zE$37QouEs{7@RlTuJpJ_!7c_q_n@ZxWOUazdZU zyJ_;xAH$h7pTYu2l9@15WgiLzwwE2-8 z3A+rH7P}1LZm~;n8fxgijWza~Z}u#$P2P}Wnr6@mK`vhfyDWH}7tm_uiphwqqZdd;WsmM)jX_YOi1Y!L zeK49YKY;s!7A)aK2kD!1mwK=3012;3|84(U4pq##O52jF2H!ffhSvy+vyecQh!!T= z^38~3@1^>~O2FA{Gov2o zv=&Hu#o`GXPAR9A-bBv}5VJy7CH}!^wIj}AKD{{K&Xyz@TLO3WKTj2>^aJ&&((Yt7 zBrTQ5+tJ6>`UAPFY`ey{WtfpKeZ|wLqb+&oca6_VZ0CvUV}o66FmQQ@L6Qvvw}pP5 zb25v#E~BdqD(o+RMk?8m^8~hRrIfY1G+GI2#HKo%E>?9P%ys-HQu9i`zh9a6m zX;bN9DdbJmUS>xuaZ4bUCz92XRKZUpX+4N)mo!on-b?EpmQwW6JanV?@=q3;2z7Sym}*j52k`T=d4V?c14%ckM7~;}>*%^l?q)=NkD0~%H!+r=0P~RSK zyAp_iHN+`!s2ZFIJX1(hH>eD034a${W2**+SAj<;Js^LM|6m|t33eT)t)rGyz04U0 zah1=Oo`95dx|N9++mjv??A9?9W=b{DhbOyoV<%^lou-@GRTwD@$bKQ#iXnmLyFj-& zDgm>$KGe7~>_JM=O|od~@eemXR*ie+D<4DIz0&O4;PTrYM@JG=Xd5_WTG=P<&uLHf z4N!?Ks@?Eit$b>lb!mUai}q$6o_`5^SSdjq^(F^0K_=|7P-E5z%r3Yh-3#@gLfhKI z5n6V@9|FkTH=s{@<@ebz7HVqi(5?dNU`Wer_VO@d$~xmVIVk>Qo@%v*siX5ue=CoFbnN6(_S_ z6eq%?D^78tJf%5t4(;|eCuxs>XQDZ!B(5|ksrx|qqB&VupbC!ZtU6InKUa5BUC>k~ zpd{EXXV#|bbR+P0q&k(!`%%@2xr`k@M|CQcJ?p zLqGU34m>_via9D^i-x$mYK|N*g8R8jru!@Q6+lH&G=Zj@>~669Cl$c^l*4~R z{g3J<_q!A%4MW^ps^pnma`;~Z; zY?%(nDo8rLts^I%`tn-tYmLpbT!a&)jM(Uh^A){w>?Ud7kL?E{9`m@lg5)2cpEAfY zq0pz3$JND!5f~dLd(!5s99j?BQYr5^I&R<48&nshv6|Q+kD5+|u7yrX8Aw(ZjJZ_E z@FL_?@QLuXBr3?~6mK{{uq%*Yd6hS%RZj{cnYOyn#61|1n{rV7%sfdcWvXkvjC~*6 zhl@9?J8Gf(C{;7{^@dOu3HI zrq63RRPbpGO5E}O?tzEH_m+!L)o3N3nWg}3x{~s;sA0zu;%DIci`y{hQ}NI~2=_LS zFLpjPhmRxmwt+H_&d1RT{^5*6QH^T=Bz>;{>;1(pMZugaLBn&WM-Yzqj1P_XVrL^2 zw5rcF)BvRE;ox6vMZ|{rSGMI+uhDsUpsr7_>mn|_^&?DuJxq>i6wEK^niJ-zLyRCxPs<5tel_O$i2OQ;FGO)s5Zal``)UNle{N~+{J zApK~;buKIg;#Z9bCL;z_J~CA&Z+e{>?2Qg{(SNKnc?d)YM5uuI^u+$!^wiT#BmHk< zk{hwib&}Jfk4diYwCL+-5+?e0Ob@?a>aky*U%%F9J&RvAQ&JJ)6Bb_Q(UU1zX{ZtN zC#}hi0AZ8(0yiIf*PxtEL{8L1L`RP6>7fvjEOg4IPTuS%$sE4seVIcHksns% zl+fY3{+Q6wN9MDHjyAYywu^_Sp1pZL%sGIy1h)%j4JMakPJk!uIv*9Ms_CHs<2 z$@wi=fL#!G6$41urQr(D9-$f>$P#VR7FEpd(CKZ*YHQd~fw^_TKN|!aIRukN(aHnv zZ>157s|gn57wi+5>lZgw1rE^I*L-rF0N5|kNw<3mw0-PrRSIlq-P5N{OSbW8Ls@#? zzxI8GHgVzi{)d`SXM!~)WUp&lfv?pGZ1e_+Mm?U-e;ds*9Ql*-0sfwN~N zNr1tYRTLPcR&tyYS|yO&1CnZzx5;;W!GeV{Sm;M-0B%n5DFDOeQ)GA~heHIK=?lhf z40U1`xDnCTh;!e-95ZZED*)Pe3F5;_eb4rlN`1@9><<~ze=DPSeg4=~+%pz*Jq5)s z4<4_=QG;CYDOj1t*bodCz!c&n=t6i%(W@lXXaI|?tldDLp8Vo*T-;S9tE4J}zZS9!5Pc!pt9bCV|11T*2HgH-Y){}2Uc z(N3&9`+msEt8`*!<*AT8VC5NFHNq3D9JDQr*C$yy!5u|-X60xufV#!XxrvK!NAfgI zE`w(s={)$ud1K|}TH`jWWVxfX^L%ULMco>dO9Wjt3o#C5VNTOPre zKKaG{hk6hkw%c5A<6a@Q41 z#veaW4zGoKouPiOjAu7I3+#8icx6%3WInUTWu1}OjCevIgOm8Qzy!)^pyZ2vip z{?9k5ZIFkv67nnZ!_&wQEAV4c@uhHow2y^iHwmXx;t>^xDF2?QSeiY#xj};pCdRpX zD-6rxx=kff($v|_q^=rp2^c!pvgS4OysPM24;xX+L?Gpi$Rm+_58j^gAZL~_6GMZA zsBEEJ=FHB(TL_QdR>48{mW`pwY|;mMAA0aeFnv~;-iAF^MmF?_A(svw9Q-z!>z7Hu z*S823zs^FEv9%o&{bcJhcA2O0Ue5YLyjj*E`b2`b0iWq|bNa>(H$Q}LIb;%m6|YT} zIy?C8Lw{C(xNJ18ZQ0l{ZPTGy&5PY)+QE>symsi?P;~iaUyQYVncnT$Gx|q~O$468qsiCf`3Jxa3Fx!eU^SDI-hjV!NKARrf`RgtAE{(@ZJhf4H!HDFFozF5z*+ckXC z7vpg_7i*fb4z62Gr))ciqoNRs#cpcu`D;0G{%ne6AV? zXd|*2NY|wsR0HeSUGLhe6}Wjn|J$#wEm)bv*1O-;3uFg$$CS+H`+7lbWj8UlpI?_e z@6)y-NLb&UrX%=yxvb>lE}ynf5Sc^5EuA3NMIGz?I3`hxnt8n|Od&?Bb*{(X+U~Y; z`;QN_wWJsoR@pSGn&%fyE-~E@rA6#lb=rQ%Ze6ZZ9)(t~&YzrqsAHb~(c}9%kLeGZ z*pMxCs1?mQNA+a^PT2giex~Zn7IHj#^;pDu_W?aB6#C=7M>q7SYw_}GEds7zf>MsUFXZuU73=isQbqXKl5<(=WQL~FJil-$ntpo-`j^3;~C86ab>8V z!FEL%1_Qcj8&hI-ef(gk3q^!7j#%IL#7?P$A~wQiMG>;Xk$-XBqyp8t9lUwWVdlC` z2^fFclEl-yk6f<}f#))%A+(TWNnbX$Tcp6Il8^^*jFQf4A?v+lR12vah#Out2= z!lNE5lO%Nsb(-?NX=jcT#B!Q|mbtpk<#N}uQi|U#PhUSP6jy)gJ2XAJLx~N;t#-ZD zl^2iDmE(Y^hQl2s*klbJ+Zu%HZBX_T?-{iu^vpw#*;U2tYf(yty1~r9VM?d)XzEzA9Do8l)?qq z#N<*il4jVIh%J?N!=V3|W3XHFjjdzhOQJ+xRcbDmX>`~&pFvU^Z<>x^xeoO76af9? zBlwKN4QWGl!Pj{5gv;63MgVUrp{HkiR%&Mraia$>P=O9rO!VfH^RIfljfokpNz0#k z*!z5zj@@=`-M_I}>-itD{xH+DkbXLiwMcI#wyeO$LpNYTCR*EH&04zt^ogwc(3t7^ z(3j^_uXS+G;#88{1Tz&NMXlEEh0k8nzf236l#ql}n(5A=6eqp?0&1l|O}qezRMPns zbAf)jf1@S{V!_3A<$!UO`bTo`_;h7tD+E(6mkjn@LZbH?Q|u&GfC;&VL^OsLE)-Yh zL(1AJeQzk7^)e;D6|TADGrDbU4o&_R_Br)G6E(r9j&|!@M89sO!$rbkCbV`*C~b5) zx)Y|b+OL*>pZqp?M)w?aA{VmLOyc&9;xh%niDjs+0|=I$uIGN;vN&X@#L*Yk8IFvl z%8lH6@=DbH30cYSORe8nNo`keB*G{V_f`Nj!CDGL7R{Lj3@X@`Aor_XIt!IFxdRVC`p>42A zr2$9Hy*dsgGHFlWcv2z*{7d2eu|$43;UY=>auzs8(xs-ne(=fVOCn-Ba;2!WMa9Om~k+*@tflKYXG8#!KxW=^9YW1o|u zxlZU|q*7wEQ^wjztDFbN-zOtyqROj3{_$!rEWX#kXgE^WqplY&n?S0XZy-Ke1B2kl zf|g3r8v`W}mNMdY%9}<;$;0Sfkk$o-#XUL~Q1Dvgg0*kK_?7z_40mr@FhIEbJ62fn zKGO15p;YYwTTrtTVrf;Ry-h>2F(A51njm_xw#smL+dDi=L;a5yvhEIQ`REGVk~CW5 zr{AXGNutjs4irQ>OixsC3)b|J3YBKKpu|bvKc>y&?yOW@4?-W91q^eD*Kqt*q`y-2fsMS zyqpVZUh5B@tUvf21WY$LUbVDpX|$V~Lz-$~N`-Z`E6W|kPO|R7nt}AiptRJ4B9#@6h2qH=eon*qa_XM0&Lw{3`q?j=y;c ziysLWU5yW3h5O6%qt`l|XYnI3Mv=x!Ta}>+FGF({MzrQ12~-B1nAu;Au^8?GJ%Wg0 zha`_un>Z+BHOss1V|=jvFm4`DVPW);@)#}!Z%Y%7a)>MT(Y`||Y?3AeN1YIw_9f=x zy&&H-f+>Krl>NkKEN1vd)#>F`x_+?k1HlhWS@hLxmC*>L>P4rOkful`%p;HqOHkF; z&>KZ5)dzBvdyTY(1QvxK6P5Py9_!=Nkpd>mUBab-#7?re<1q~qJf4~!G-o%QO4WCaT2=Crb2a< zXmI~Vgx9|75f^_Frj9<`sHE`r?o0;*3RS>y__^c-Utc0zha-(j5lG-&cc2ab=7_Q_ zt6+5~Of2^(+&p0)4LTLJ9ufiZNOJx>3|9X4%j8z%fIFvX5);W0NmFHO2gg@xZeWVQ zGv_?9 zG4}fMGnv4SA7-VZLS0=N%}5goLrAHwSGEh~rZ{#+t4burtLes*Q#lrK!;d#DA~fWG zzSd#s{zHGq`{k`7B4{}9PFRRSG0ufdNW{V0P&_wt4P=HzbTz~2wMRi0Y%esL%O$p0 zmNZLC$_30Xt+X^^xT|~;piy{}14-?%oZ)w-c!A4kJe1Z+acl?oKOWKqL9M8K42;=X z(1!<41xDovXxDY+C6;Zu!JX7m2dLkXN5RYiNwVDGS?v}%Ko>BgeVKr#lHxNi^s3p# zz*IQmb$wdlu$dPKlXM`3YmEOkQHqVHdBbyEs@^ z9n(0^2~UX^5k?Z8JQ*v$ySl##$OHLi*2EtcB27k-g4NcoE}7~n-0@#k%vWpqsN@#M zBmV}SDu65{>6)BK->zx|j4vsl)|AO2OBAwTcX-~7{FyMlaDF64Mv?4SVNW(Yc{k1^ z(1z=)GbbEdyOYh5=1izZyfI=fZ3^GUAMW_V3HSb-qdHsp(F$l&z>z=Y6xB!-zh)$5 z4X9quRcarKrqzWc0fdo2V5a&8a5sGN=oDfq-19-Y*)eueKm&<|{+ux@YSDv8E6GG% zP;rPLqv`ck+s*l(&JEH{2-z?aYjJ00PDUCiB=&5VVW4Mqyns>ea0rd-43tkgseMQ5{N!#ZbCR>Vh3 zPm8YdX-$+!#}Cn`z?3!IL+&TFs|kLTiwwf4WHb41kvYuXYHsLwU|tBv0WUoxiFk{m zL4|NCkrL8_ryrW8N80e}Q$*qj?ct@-K4>f6;jDx+&@&cbDLx!@UUCya{O0M?{2XB!PQn&~S4f;UzXLf25w=FHZ|=FC+jg zTT|cLv{8W~Nfw3u-C@Ctk`OJm6 z(Zmi=TZY@Pd-F!$2h?>J{P88)xaZ8DvGzDSL1;|@$_31yzsL-k^+hWzDP70Ow^Bj3c_xMi|4xB{oazrdXw ziyh!&gj#%D*mW%SHZgoZ2XuE=eh`MsXp zFZY9_O+B)WKO|Pip&x`hVOM^TVAC;oKHCq%t*EW&$NV68b-ByF+z-Nmg9<oxs-}`hw2#50%evoYT z(9)tIMoIf3KZrn3)YXJ{q7G38g|09hyojSGj*za0#t~AA=qCRV5Q%4QgMSpu!9U>7 z75mVTAX@Qh{!yGQffi8BVX}THj=W4{1v`pkBI^8((iPRE={G6KYzmG&7hw7vM+i4; zQ`E82SO{^1h}+MujY@oiBLw5fyY?I* z@nJPj$i7(H_Jpkc=^EqxhK`VkvB~Z6aZkts_&qF)CmbQohUi*A{5ehfFGsul{7v@r zl=@R|Q?!r0zU>Yq#*saTvWntyMX)Tc9T1kVS*& z&ZobWdTLe*Dc(!ko?WRXrV)E35G%w|^$> zD^GYo#y&TPxlifcH1~3Vp5Lo6TTP8_FRebp1?Yflcu2)Hy=9m=6Hs_de{QvF$tMmd z`1C0&?DG2BIi+B_eVx|$HP34dP7^zVk(<9)kmCHXDr`LDV^8kLy5o}zDMPnd{*h*J zwZi!5J)w>1ldFD)Eifg*Pj6dikj1C;0bCwq!Fn+gepEsI6o@gVK-nT17GBq)aaF|SN^#ds|{H(y=p#^aXGZ z3ijSs_nl=bweTstnvU~uIZf-{vy%t!D0V3`gIyPt5oAn(*+I|s7sGQ+|${gK4mj}uU~_o^+8Z;uY3Vu#dU^Hi6*cBrN8zv_;^rVVqVOh>p|rP`DEttB z(9B`3@%UKb$C%ItRSmX1p%oU}KK!ait;(^dqfWWQ3a$-KPd;_jzvRhl#O~!!!YdZV zvrfXx4Vq$75>OI6_w^WnoPRM)YEO+i4Mp1jSbq%?oogTE!0 zdPS^w7MGeFWpPGaaR1M0T&#VSNUJ?~?2AzN^ekUQvcYF(XZ#a2*8H|2p2-0l*f#XS z-coQL9Dk2q80SdyNMw$Kem7FurW0JoJ45xo24Vzsa$1^&9R%e=pF+mgvr@nIhZ^HF zHr~F$Kc+=q-~P#ul?Y#E-_T0R+Mcq@G)Lkpx!(7(X1eRp@pM;D$BSzYSFe}|&pMOO z)*Si~=)$6r)DtewjRK}g25o8%vEgDDPDfxvQP_C!MDQ&NB>cR^RysEkYy2irpc4%t zJEt4x_!(TJftDYzk@#Vc_(%;NocM#Wwz3%Xxr^VmhJEmUGxCw%0un~yFimGBEC1clIh}oTc7TxG1(1%d}^{0 z7tNXHah)M2EPoJ{ciqz4H#3~(vgPNv{0Uyjzx><3zWfn+#dvtu5&4)-F`NX`#ts+n zL|Wb9x^n4{RHc0OgAWdM?BkjH531|@|L9nkeEs5c-R$ypRTeMZ)q$&z>BLJt9c$2& zc6z zDG%v1$U6cqiR5%q^L171Pc_&>Y>sY^V&&)ew@6&^eU{sE8JeeZ){^-dD2Pv*L?8}qPYyR4^NG6zb zkSDI>Hvz!`?V*3oL$Get4ak>FEs6$7ff^7N-`xZli3)Y0&M^7(@}aM9pgxW<#c?Tr zfkiY?TOWGRR3|=G$@V;}sR!8U3nTWgI1#Xc_VYRm3CL$M7Z*W70A2z*NjlOc3?BJu z>I`-qz$8a%WK^RF0La#s3}80cIbutF5@dcQQ+Jx5rc6-XB#q}5Lc*nTtjX`g%Azg8 z$+(yu!Ry#!pb=Tt<2pzNQp2IXD5wa8R_+VBfvN$JF2J95$tMkV2cRnntB<_A(q8IqKD0!}RZ` zAz5V>L22CZI1D*3PTYp22v$HP6NI1?j6#yQ0Mt?VRAe9AG;0ig3{-I{+2~~INz`Ph z_Xv`Nt%s8Wl>!9Porl1f@p4WP9iR#f?fU+Vzn6bPKM+7wfS0nZePnFv>cN}J0qqd* zEGRXpmcD^fQ=hGtC8#q2fB}GHz1pl*T3x>bwWeg&wuUrPDtmP?(cOcc5r2nqaegLw zEg&Nd#q`QeZ8*dog>%)S#CP69W_vJC)wY3p%LML#Bb@<I-hwp3MlQ@7q6Wyj zRBu}&#fJg>aZG}5=jc?TnN~|q>~aXE)#xG0mlqYUX=Tq671xTU@VAgTXd2u(I8GYH zVZW;79S>V>QxO<8j75fGwUDd&Lg{E=s8qr#H*GB4iGaN15USD*5i7PiPro2!eO29U z0?!iKK)J~3o{vhI**(E?0d@~32#ZoS<}V7m3Ra5=P+SZ1k&g_-{Vu>qcra$_z82P;5nF!)H8&RJrFCq^i0Ds;3VCDfRc2jRP5pd9-@Wx-R;V}KD@0l z#fyAtYT54bg@>56cc3H#^#Jhwszy=*&Q8!2J725b31aUf-+_Of z9iq}n_b^=L{$a=2YtMCCz}}D@T(Mx5fw+n^P-{txQO*`Bb)R|56U6kf%9NNH@g(9q zMcf)kUidbejn@e|J7Ublx=97Lx<@wyXbY-Hkf4Hqx{T0dPIhJWNVNwI*i_(*k`(8z zc7Y_sc$N{m$4#mb99%f1BGF^Rx&oAjm6*7y^vH+{M4PWIHFlKCXfv)E)d*3Wk|_{M{(Bj z<}ApNiUf`L9-7JB8Sz;RC;L+z2f_HxfUFR#I}mbbWBmfraVw!6vfaYnqA@h~7V1ug z5?BuIF(vq@9zfBfOQ zKmO-$zwM3w-(PMwu(?d?zB|LNyP7cDIsGW6FUfByIPE}4e@^7H?UY5(@)fBp9M`#(Rt z|K-fs9#yTl@ z&=@gSDC3R)qoCp+L~;MQ4o;XZD?d+2Gj%u*jxDvAy@{(ZA~DUR+#qAD88+qBJUIT2 zP4zi_n|RJ^cImSyiR>arv?yLE38&qY@>RfUuSGUH0Tk=FD}?smMdBe@4^*F!L^LBp zGyE^69X>t})n6g?e_*AeQCNQ`qoXY~DX>XR^*Neee@NWZ%N$w*jKGWcmrWDsLx&A8 z4gu%KcMnduDrPUw315>ApGCZf!N`Dp@3FxWt&EzA1f=YD)z*8gY_8s1-JHYYUWp$6 zKYRDK3rp-$Xs zD5lmVh4i!(*jL(%C}l=7v*_BZA6pj zm@RtacvqXAHDRMFpK=nZD4T}!D>0da!{zX#`~jN#xWD6-!}Z+&R_r_-jeX8Mqf~?|HXX+Jy-Fl&kDJpo zp$CDX_rtK2wsdN9HTg8iqHY`Z={O@8hUX3w-KTeO9WQNXQ4Wp8^ZHx(1`7RxEzGH! zElivXWv0{$ujeUE)6#ZkW-o2wEctm0Pb@CW`cUYJlAd_^Gvhb`MX_9cFr&kA5`@hu%0mx$WVuHGbBX?}m1N+&L5e zl0GV>Rnlo?CtWt-U-{(g%bgo`^;L4GPLyGlV*@=RIGF*hkL`eeK{_bSU93Xo;ii%b z=?Q!v9q?k5BF|!!DsUt&Cf2kIZ*VUSepC>=ONXs(_F3r=JJE73e-&wGtDYS2j_4N; z_%D$TqaYaG+278oLOj4>jmfCbpoME2QZ<~=$xfm`%^39veL|Fw;WQs)K5wmXm%}ADFBYuChI>Dns?T_A zh6OZy!!i*4>WjtMm>`y;fni5OYv$&!F@h_y*eue~=$Xyo7{SfbZa))IpY814(k>jF z?FPfwtnBvJ&lM>De9mR`;AKmms}aY%uDfjEfOi(F~Z1;8l|gDjeZ z$dXbN&e1&wXo^%SP^2o`Qq3z>5ui5)^SQtwFn?oHO=wJN?=Hhvkv2B^Y^=ZD?mg!z zUMd?5YpM4+WgBqVh)U=)=^`rZ+_qgvhqhG9M4y&#YYHT>xn|;-LH8b$x4C9wOxG4E zA4p&isuRwG(@)!_l)?2+t`YG4`xzN`i{bvSyMOs@SD|2vRLPj=3*G%1Y?3 z#c^iUHm!ZTo$26-M6Bz^ZErwEY76p)JqhE8Z+&baTUi zz7_*I;R{xgYNP$YaD>Sh6}ET)@a2JJ>B{g(Qq%rjAmW()hL-lHPiqn+^@EO>)_Y?N zX32XJ82DMDJ!RKIO3w!9607Y^AJ%|?!19R4@w!V_Q)kozk`||SQI&P)dI*YIl7|34 z!=DAOuykBsGV``v03iI(2CJp5X@{1(FKpDpwL6<$%CL`NExc}bE{*-tBLaq(oW|uA z*aV0+7`=~M*>rD1H?q0GQ(vWieAZ-X^Lyl=YmI{hd*;3z9CYWS0^LGsXUM0vLO#)D zxMAS%nyAc@IxPipCIT9Qcqya_bq~V73baCyWIVZjlI^MI@_Ub(wZtz#^*)m$n1nN`s+*mo2Ksz9^hT-qe zXy!=ios%oAP4plg%9-`Y-|=m|8I{TA1_yqXuz04t!WGe>n5e2&A5sW0+e<=Kfza9> z`i4j1#qFiH52^sxk(gjcj>I?HUTs6rnNvA>m2@!hm_7G=@;#G2mDpI&{c1iYYgw{2 zOCkA*O(1s3duAIBAi8NAnkI9zL@hd=k4oL*7DHS57xZE@l6KeY@qyPg@g{uWU@ZgV z{6c)-tIg#$_RHx+ITJq-9661#JrA)p?X5%Zo&pA(ZTrD?Y__F>kuxqZR@jnbbP`HA zlmS^kNHq&0z5RB+Ev!W3*M{7M@2tJ&967UKUKAU5s2+f zlyAc!K+mx-%QBJ~8)hisa;Z!cQO}f((S+2e0vXDAKjL{y{iT(9-3i`QB`oM9IP2s~ zVYFaYwKOGLXGy1Ojl<4)rq)&onPmPQb+CIThVFEUT27t0*Yk(`l6`%p&93KnVGjuf7I{25Cw#Bb<%&gV1qIHt~uy5P%N*Z?D z>>~MkM?qhjgj~egu{7+rw|SfV$gyKbyw~_hK7^8*%Om?-pMg;hOK`%s6v($fZxG!} z+nFc7rG&h}w_hb8iz_wB>F(Z-JIMk zXYbQbC)iNZrBcJf%SSVQH9B;gw3RgYMab4A=lEV)Z1+r+nHnwT46ya*pIZ&&tK)?pZz&nC(j`t| zW~Y7^0pKuKeGc6ka08C7EdsFnceW8?wutaez^&mha~|MvTW1r2sUTOw+DGF6b=^P7 z2EcEo$}B?!k158zP`pX-z(0rulgsAuF2tXljGm-e8GnxYU-y!W)9SgA%OTSAN!;?+As4#sLt_Z5*Tk;HXqoxF0Hv3v4;y(1lr@rZ(TNFtu~?fs$?VNR-nZNt zg$MgcRH@qci6HGmeY8Be>)qbgp4%QelqB745UPoBsC@z|EC%8bl(|TeyO=G|MG?6VjuNF`1$7MnE8eZe3>8qRq?A)=` zcNO~X)mVBtp5;MDTrbW0?3HuxhsU2;gNvP78EwVPk!|&e>wk15GK&!4oCLP!HlsQb^jhVkFnN)JaxA~BO=-+!3CHyNY9&@ueXpFR~*DJXB5UMd#Is937D z)}TXLcaHwEl^FZrozd#1)KYxkFM*vc;=}&bkzQIX5&insHo2G+JsZMfWjg5cA_PHjh*LaZ9(A z*9-Z;GLN`%GgM66X036^5n7g8)3l$G*+kJt!IW~z(c9EIC=cuXqm6jSmSQLD!o86v zqUyYl+=HSx6#j>Pjx$U}hTw!Kda3@Q00HGXF58q1*aN7$nE;CU_TFJjjSqQN=^6}* z@Ygi)G?CUOLBTm4?(pp@x^%STe3Tl?DQprpvNCH9?{=~PP$~xDj>AXSY2r)?9NeKL z#aF+3$Ax!~$ME4!nSxLfPW1-Unds3ow#k4P&^U$MkLA0pj3nz~-X7Zy3w z7&e!WTT>X@{bXg8i_z4Z->ksRhK}gVfaaV}sd>@Yul00hRd;E7P6y2{k*#gjCITig zBhdP`3C`?+Gh3kTfaL?9f8X=Z>v#19o?$sz;Te{bCEhn20N=dcQmJQHaI93Hb7sZ$ zTlQ(!c!k-)4O6&1Snu{!K1Txg541BTG>nn$;*q@nYDWl2>OxbllJ( zD7H2PAO(>)ebbYt!e22ZKEf~X=%Mz=VXau%M{bAB|ydKs4S!F?Y3 zlizUvZEslzuiAg1uiJ*J(^4BGwqwZ#ydNF<@cvWV(?Gjhi-+)j$2ZDCXW;jib|JBH z-cm=4qcz4jC*3D5V$P3=j5xtKE3Gn`<1i3^i4$eE=WWfIuQ0p>6C-gTPO0<_VtmKL zqeL4mJjZ{0EhFvD(l&^)MR^$RPEcNM(vcW!P0PJQub#zgpkaWCs#NRn+`cnkyK&zT zce1^q{9mHd8ksOj#JX3{_6?1!~s@?q^sFK8yqK6ycM zea}kBGkRYY4nbinGhwmb;rph>VaS%YMQ|DOXd%#R8$x!uKgF#8OTePKFhP+qwJmpd z`En*SG_3FCvHf934_;cAM}JDqNnoZTp?EW9rtJ*?`#Rx8IZ*vJQPToG>}k{!+O=A1 z(G)JJ%9%`UHMi9`%V(tJ3+&s3K>02n4mV5{vDd?vPw=v=1#?3iqyHD5SXdAPj|(j@}&IMd9ggSxAs9l zTj(zXm#fAanP+ilbv^zl_2wPw~*E9bTabmCQ-_N?I1!{{y>ikj0J z0BGO`1F{88lwRSW7olh0O2P=mm@sS55y>9`v|ZXbuNv9%aESCa)vHrz8;?btY3{aL zqYF9C0dH}o@h-Edt>>Iej17wZO3DjZM$p>ng98l}Q=g90W^?2tQ`@1ISU3 z_Neoha@}YAUBf0A{AlMwOiSBxUiD3|DDhYKH*aq+$JcFwlS0x9{#PCq+}KK@g>v`z zso?p=OvrG5aQW*auUTvRr6l9MLw!}9iP}uqJN-X8DHmO8oje6#_M`GQy>WPQJA<#k zUT+AwSFP7mVxZIYT1Wy}K!ePFlJ-IxO3;u;LV`jhaT3K!GvgyrHgri8DUy^xSDG=X zZs{W84hO$$4%*09m^5fNnTbmycvhS~k0TT#+L5TA#36|~sZo+VRL2ELE#nAAt{Y_u zlYsS#pKfSTMD;?tea5#saJ&rScv;>x2vg1OhQ=DyxDM$LVEuf}?!e;Nuf2|UL&cvj z+TF~q>u-`9Kw9Wz~l#gqvT8(X45L3O5ZNa9n488+~bF~us_5rE2kPyHW-w>W}8Nl zO0NXq<1sA)=_G{@jddwF@FKOqFtpVH%+S8FJ2Q2q#_puY8Q`VA)1c=o`gkr$;@96q zBlhK1q2;gt{+EkDyVJ?GcqA^#_e@6UI(}*qDLe%aoiOMFxU2-n4aFh|yWr|R)sN%> zP^NOHbuiR!uy)!VR^@!loIDYdu~EL~ECGPcdh7RaP(^zONgbp)Go*>`tof4WLtB^T zJX@lQkDIlgab&VBV$in--R0im-)T1tM?!1zoq8oS`LKL?`UIW+Fp-EMHnaJr9u;CG zkCi?)Hnpc^aOaL>aOd829OEfd+FAOIdPjTUo8VTrH*CwRj-^ew?pl)J=;eGC5}$Hm znsU{k9v3rYo{`|4+N)DAaFM_>Fm}MSws)`@21(uT7$C7x)$F(=DO@y&gx;d?$ql51 z&1ILgeb($hJ*{#YJv*(Y#_6=$CL3Yy&~%tjX4_Bm7xn|@jAv_-ZU%Ljl{O3+>gD67 ztBb?mN_k%W=-S;tEU(*WT+nN&LS`1{Nr3;3A$29HWCyPO5q+_7R5-6zN>4l`_4OgF zNo*BA+dYOs1acz};&GBzfs=n}l6fGn!nr1YUZMr^?=xAwXRPzey_>q%#leNn^$Nn1 zH%UU?u%e%#JA}Duc#3oS0-fW?R&l_c(&D%BHl*rH>XFDHo=YV63SaH6GrU4h(y==_B0U&Fk#i;+3fEq71C1@ zo#z#dTjP#5Gd5(rgOlPs2H|VWPbA{0%rnqU6vPYE^xNTQ?a80|++)D#*ZsxGSxvYN z=)ce0%MQlAV#f|VdmHI1SqPVYXz|070mhL~#7c-Ge%Q8d`x?F1_jw&y=|s4MuZ$_8 z1Y5<35pM_nqIgyXKn_bwf5e&3>)*6M`4)qy(lk$T`HL)lrF?xu(_nYQ8of%sqBY~o zQBr$8Ij7CEpjwxiUD_rA7N6>{X1-W)>E6$-eSC>IotVRAQS`;i2j#@}ZVux9S&PH! z`U=4fQ1iM4ONLd9cv`ShQ>TM5lq-!P=m2>(CgiL_SRGl z#;#`f4bjuUocFB2hs_)tz}+-Di)K}JYE9_aQ8*q4%k!G@JN7(nRV;DP+7soHXS?tF z<`7o;*Vjt@+S{h0ZtIpWGks|D2KPZ4hW?qP7{CC-&<_I>(5+OsXO5?89G*;1`0Ek9 zzzJWnT-vHcmgm)CnVjA<#qRa^)FyiQg6O}7l27wuZ-X0}N}3M6U7pgqyR0kf)_y7GL|MSP+-QD{i&(ZXc-`)M|U;g;JzkU3_|M`JHJI%V6gg^XWw`bzM zgLC8L&#SigtzZx3(zC)>*2ZTT};Z*@w?8Jzx`5utv z01CXr?kj@igcuw9pO-yG)#<}F9Vw6)F)cIn;Ok zYG^$Dj(H&JGui;C(KL@rvP$PsdDpMT&-n87_v|yqc`3LiogZq{NZ#KI`Z_L?LnfhR zbJwqi#?udK1ClRV24EhYv8n%Y{2u>!*N=;1UX?Xgu9iYL;)I3CBm1- z5_jiazZx1(zN3#szGM0BJ}hHn|MT)C_rE~iJ^PYzW*CRzfx_j)UCL#8pK{Zvoy1bD zZ+xcn)zEnQMQu#-Mau~E0U8_opNB8{C1=InHpjHX=`)>z*)mDLKx4(2#IYLc+jwuW zpV~bf+;{zIXgv9nJ{I*Q{mc8n_>$h(ua+&P?{n42r9K6Gw7+AVE#dZyT^-p#vE+5?~=}c4vy*f(-@)KA`n&_>;^Kar?M>KV|Pl z01KYjdCWPKdZu)xtTK}I%7;~=D{nn)aUx6G;Yh&6g-Dt)4O0>jy=X{Eb?aD_xscF1 zWd%WufDXd439%3Dw z`~{_y;pZgr_CEfZ!$7*pPi7;KdO>!$b&5{{*!aWgtJ}@E-a@Qyxh3P?Nk@@& z2!)!OwP9ZouPCtZL-4I zjI-_kuFcc1rCO7@P8yX@u02KWSHXkkE+6+)TjPTWGTW9;N*`Fx$wg8kGnOXmW`Y%_E= z2}&0|zQ1F#=k0i$`j*9CONi6HTw;7ASZAcrZJf7vf=kvqihjeoV=wKF*J`%fd^*(O zoV7K*)ObyA&2S1dMC)kpJ>GR$ypy-DULAE|dUt9LO)t;RMR-Y}JK5@v`Te+KQH;h# zyiXzVk0YV;b@4(%AnVoxa^zMQp;6N;>DK4oZNSlbh1` zlso7aapm@3zRQbfu`&nR&9t2PF<#T(QdF$@;q>Qx$K2sOdujZCHeCOT$1uz*x?!P( z&Qy-rCf^A_ia5q@p=qTg(9)8>#8v0b?S`Qk&~#f0N!0E0zn|JKJ-YoV+i7}qZ)v}d zIh4Kt)Hv(rpEb`!M<%>W+#>shue&K_=C)^^khn%Wy9qW+}U%xqQzVKMh;nfPZjn>qe}?}x8UQ6=WY)(UQ;*=9EL zcQUor)JC|?Z6=_PfBR$2;PKC2KfUwH={@>-^BetDoDTAainDcc?mveA7&CMPQfZ6M zrjL&_bWFa{iX5(1>Gtts|5^LhxSY!O(wEa!MA9?V?;w&eD${N$Z&4i+IAs7maa4o` zo&p<@6a^O@QPh40307yjW@zk}5lQXFyY^F$ZTIu)_b2sB>h;|Csb6_ee@=fVMS1?= z`1aoTK4oj9LKPpD_$R$P=1Cmcn6Rtn&?Cc!#EMG?Bqk<$j$YoKv5mJ=%|?o4K(nk> zsKGE_aGro`Wl?9i;7P38`D$sSjLvr$x8?6fV7`59?0^4AT!88YKHI+V1AG`AKLxv; z(VYGp3G6kt4|@iECdKA6B~~6I(Q8W-OCQ@kBxS7GF8$=Ys1Od*Z|oQm=ATNC0epyc zl2^rT&SWrd+_zKN%c*7li=j%rCgs}{oHBg-0NeeV^F!)bl=w8zqZw3azMf(PuV6JD zg4rNSW5vg-4p!`)c&YAk8UMcq3JJq2sE1oMaZb-z3sF*+ryi|@i`B$4u-C$D5b zk8+B1Ep54y_?+a?=oEF=9kEkCG@$d%ckNsQD|ve0M-z-v9ze{fd_>v zYNoRxI2;`C3<~T=-reS5o`z_?N*&UbO&wYHc!!&t--pAeExKjyT8q%#yojTe&*+Td zD^`D5{d`@mY(L_$e?Pm42PueQniEQ4D8rr(?suDD;9Fm1##jpWO3j&IpXJOUq)-OG z98DTMKjI3T-?_m1kc$;6migZv1P4oc%n+hrQ)GTkO;4EG*Fe84P#cBnm$h&9*bo** zg8bNpq@ZX6Eh;I|E)8b71?L-Z*%fu#{fM?q;5<>B+vvSAY`q_$e@1UU@cU*x%H68n z^>BnRi=hraESqXskCx`#c;%Tt!sc{&^JDb=2<|Zl-G@~x{7HLrNX>hmIiR+!&9&Xp zfoeOdA2BHm#y`~3Tn6;Hs(e9-kb`R1Mx9WJQAPnbXR@txfb!CA`A3~KT%qb9(b*m7 zo48JSenI7d;X^QKOM680L;^4SQ0$~npjbjShjtNDWJ1M{ivOW#i;SM`=B zrNN;=Y2b?6(3bRztmN<}2*DOr7o$h80`)4cw6uv?h}rh5p+mH;Jcc`Ucjq4Y<3^+Z z@UCAiZTPh38ysQF-&3Ty?jGCvpHC*Mx@#GpHm+`DHen`wLor{v3SW-nkrvP z#>Pm@qHu%?x0^6_rr;b~%U@G*ogj|InFEi=7Nui@YI8I5H3f<1t3`K`qn)T#!d+b2 z=uM=qSy*Aua+nKg$w8E5B^t5YCEnGVp6cgfwvL%tW{g`^bE}Xuz)Syq|2d;LVQP^z z3k@KH%VGNtTb6KO6|)kP2GR)2N`O8)yw*<%kK?|gY4AdDOl;$5@mjRN80C}0D~qm} zwONz0!HiCYp4ly_(&%P6`wt0;xi!T6VCCZcysMOfgS`G&6pnSFUsi^n8J?6v3x<(P z0}n92EsSwg9V9WzSKo6A1Lz576c~anFSNnv85)yT!veX7WSA!<51i+z4$_*H9HjG1 zjT5xYRksJbrgj<&PEx3^2n|{Zg`ff0vW5>-BG`pY2CXdCt5SsV>m-c8xE5tf3L?ai z4)JVIFFL$=DL^LRpE||+u@G$URhL_*y<$8YGeOd$UM;hcR8Gr)#VFVz^h=?n3$pdu z&<5;KusLQPC31D9Q(gQm^)~DSh!NC38B+#31Mp7hXdkpyls(pb3TIEB*{%K~F3zgd zcwL+EMW^F!cmGy*clREg?xSzIyD!R|A9HvAd~J`=Vf3e+Mmg*3cKntjIzHvuJj|JJ z;Ib}VwIAKeXii5rbkBG)oIAS9W2ziu`Dn4l>>*9#;0J$mvdO;l(u9vVJcE98h5<0C z(&^wp4H)a&!Gv)V7A^(m)V%a4ND?Tc$C#VD^@cr8D^wX2bi;}253JX^;KVK*j}&&e zB0?X+NSHN0K0xi%?a@)Z0TONZu3n1K1`>z5fWj5DLDytAI@Z&*Es@`wp5lmwxX{Nq z%vDMibr9AEr1JiBZ3~7IHz2(SSf@F2pF~QOsf<43YRfIKx(ggdr=r%<9>Vttm>3ky zp>TOzM{DIlLDY1Sj{yxpm8_&6n3cRW@dgh}=$#f-35ZQk1l#CZ<{)lbHmu%n+Q z-8tkXZU-;Tbn%0z>>(lp6y!l3?}DENKCnosfUkG?D8rp0JM*FZ>^0@_vzuzDZ{g$I zK%II%oX@AhJoD0;x|;d8C!WpS^9j%KDOSt>VytM5;MnmEoGg?K{~DWn0K)EclKfLQ2@KyelU7^YPeeKa~PL`_^^aYIx1U_z~qz`#sDi;89) zAGbh_pjf=m{Mu9w^VjG9qU;>xK*$iIxU0rsA&N;ntOvucHO+g#BFz>M7n*l~1ynmg z^ln)fU9o9PDgnw`Q!sdKO$lY>cTu!6-+lM=n}@F`a+tn~uMvnnjDGZ+)cj3L&5QQL zO=|u&q~?^W(&bG*HGeFrxmo>O#gMNUeMI>Vw~Lu(F%mh4|0(^Mu1&qhn>hb!=;Y6-ww%rXgb78|zdD zLD?rj?)6S}0N@lTb%N7rCV0I34IyU-F~9+L0<|%UU4XIMIGH$Ig3k+2+dk?5jRNFg z#}VTwcevwM7XhM}LN`;4grQ_D^|P(V9G}@-NhgSUb7}aZi|#=9-cb*dJmg>vum|>^ z--=Jwcovy_^U>0s`Hg^u9zO-S?lX!cR822aveWXubI=Rf<(536c5e{Qm^;8kKE$LS*>5EF|xrK8i z5w5+YFFQEC*^);2b$yS(7;QgY(n&mCPg!B{=7W~4u#bz?2pB`%45q-+w#d@HH9KzS zRE`Y+=}inaE_HkbEnYo5et9UC!5$5MVizP_KiM6-yWGfbEy)|p0YFU)ilKGIz)8&g zr*iJ!Lj*ZtVs69mIAQ`3-GKl*9K^j7?vBUbmy@8P4Wv1%yS&VD`(orU*V# z2tBmt_zRzQYXGo~PeYn^S{VD~QgE^6@vV$(IINieXmR5dX8Q58aL7X)Jj!K>)$WRg znD~40hY;wJmyEv(PgIkHyK*78VK-9hP3Ko!6%`)$S<3Mw)V388s3G*l@?#SwcQz&F zGUF;F^hqA#(NL@m&L6sdee40eAC#k}3<5M1v?@trUnGVIPE(j=!l+hVCZW~!>r zN{W)EBm)%lU^oo<`1j}#`ds8`2@Cn&3gsL4GS}%;k~9hXoE34G*X4vlST2`!HYMFY z*Ubi~P645Jn@;vrJfs{7C|M4z#^S_7+8X=gFG`5v@xwNmA^`RLcFfMvtm*l!u|MA; z+CKbw31oKFa(I4i6K;);a{H3!zlAs%fe%*_Cl}SCn>e|NlZf`OB~C7xdc%JF72@Qg z4m~{n*NBt9{OMRB{_5i7Eo{gm3`$oX`=?AsHw(@@xyDMyX><<~t)YR-eI) zwc&6&$h4i?90kte*pn~_#RDG%@MKPgU8PLOL_ek@@8*xWF~PxmTZ#Cd9%gw(Ut=UvXl2*?Y!CUq+wAG#PI<&d9xY* z!uXXfNhgpj@$f_s5C};E1Qtoz(2{=vA85Zo8!#BjU_|7|4;&L#1?H$q1)8Xo7kVjp zg+`!iDOV417{fMrSx_iG0=6J{NKa#rOWvd|QPduQEFrO@^q4vNU}7Fgou(G*m0*wA zkl(TQWNM^@W<%MS=?V9X!oNVT80`v){j$*6jGO&#o~ zF4E7ezTlc__GoGZdY`JPwVc4bL9`NHrL3CZ;cauJfhf}oRYzt5#fc^j4uw73=4ckyw`gO zy*M~i;>DrC-VeJ4Ct(2atim{3xi~pJBk74v#~f3bOGL>rqFw}((Ne-WbyYM5#+6jc zIVjy4v1&L02=4{Abq*pWjqBl_CzY^;fG6Z7GZKdhY2w5dbmk;woQ5z){Uv#dq!`t+wRrczDe&l!5Lj%ExS{aYw8__NqJVAC-X zSZ0YZ1?~-Cw1A}!09Zj#@yRB8yRcRwk?&%gY${K5%7|u6rhpi(xB_Xh8Te2OqlIDY zi9~=!E6$VPr4j~oEV>Bdi&TWxjujP4>5$~+CMbU>>s40xWWC0?EaTPpm8EJRl7Yu& zJu+T@9a>Ipx*07O&dnlk!m{SPWI^+cQ&wmU4t1b=^dLdY1!OhNU>xi$i$RP6emI^z>l7{rHa@}SaJ@r z7x<3}my2fwn;o^SydB6ZV_xrU)PzFJ8k7BDa*`yjUrlh9Q8g)G^v zEc`+1%#UFd_#>94>DSUqmfz>;(+wUWkJspL*do;rqUr2WeJsgd4Ou^2#s4_>Efj^?i+0+1t7Dop?omQ|HR1pylmcc?F>G zcCNg1u3U^*-_DhP{rK^Zzf-m5>IfTokHfigCXYIEAy*@i_nh%m5+u+eGiq5Al_67U z6LC^i-o%oJGNmi9Rmz_?Ww1O{|+NJ}O4^qijPETsu{MR2klq6v0Iu=Kx<@QflT z?A3>9ihJ$1LVa;gh8;Lhm1q;~5X&^G&lGE9ZzXe68!d+^E!3A}Tx?WfPtB((t0`$W zF}ec71YwXyd)KiHbBG7|4Y@O0uUnQi74&;s&cw5w;FSQ==$p7e#JTzb|xa`*Xi{ zxTW7bU@pC{LQIu19Rv_pX)tn&|%Uf0d?z?oX0q6p|f% zf%BFZk47-LKt{Sh@UZaKKd1XUnq0W&V0}-QdjFu)Z|Z8Z$t$$K#>3BHzaP)M$T;8L4yn=A_fa}jQNam(N= zZOh>G=U)sWE(hQ6`2Nu0@k`tC`1iG{@Es+*vGi{FX}=l=t~U<7dxOZ}OVRqDs)*d1>W-*|W_ZoY9iB;6@82-ZqLFScr@_N$EVyw zp|e)(`;c9TaKY-rIf7>pr*JngzFqwMQ;*}me`=*MQDQs&?Ld)xD1Nc3sr}t zUIlMwc*5fDJ6+ZR(}@pGyyk!ka;cDP^sgy(7EJm`s8S;}y$@0?6@cc0n#*Mm>D6xy z3B9skE2VOB!`@IndrCE$H+?`69~^W$p?lA}Z{&rZ8^)Z=kIj(c>KSKbdT83tJ^y6M zca0cQ&T+U4Ca)kIGWraR8SGqZS@5hSsR+ucBGf-Q)L+|Z5L#uNvA+cs1Mmq>0bQgQa6E379kX0W28{I zc)6NOTO{7Rl>noZO6t$UjMf&;BmUt&f@!QF?EBy^fAXCJ%&@Q1d_7f-%vhFbkC^xU z^r1G{7Ai#481|>j<@xOE@w=~KU_QJQa=#r(*Ey0d+X>(Mk#tc<9G3jAI+FN^4nswY zzNKzQ(!=9F!jW`bv#C$-k9H&pIv$~H`-gnvBk6L+=^|Nv)saLITK#Ep?O)|cI*QhJ zCAbf9+++$Jh3}El$g8FN1MOB+`Tib(K^#RPEs9GB3lJr?bc3+uD zR$QOR+lu?DnTEv85O92!K@ny$6f5zMB!i;NZ|;)Nm@4+pVqx{NU6^8Q7@{ghimY`k zxojAASL|HaN- zP;u*&G_1R0e%AxJsmPyp@9>z#CcA_cjRFj04WF$}5eqgP#ebeOuA(@MQT*Gcn&N%q8u?x~)y0I^ zue7PU4teSc`_VVm_498b2VXd;WE)54<|pQUcPh!G<_Ax5jrfE_W8ymH%<2>(&#BdL zjC&P^lDZuJ5GmOZjmaWM8=@^&T#1A~2`!&oe0&jXC8Vo$O3}lvIl)eO7^e^gFKNYa zlX@N=v;wgv<%uIx;gwD5lT>ay?si2_h`LAToq$XDFL}JlFv3$EPi+(br3A#Ov4ZDX zi=|UJ+%DO=y7+8yaYZVON1Hzyo&JQUq`j$xm*Me41gCcjCG(jl`CpJ5r#SwSx#MXu zGP}bxG)guV)+*)?6hUIu1}v@9&~S=Us?E+aG}gMNO8Jt;KMhTWWEz}!)cYr0^iK(w zq%J`6{D)&?csG(cX?QWhVq|6L96XT9ULCKm{`0l3oY>O*^WjdEl*~(s^8lVhVrL;C zniCRYC-9ofBMo*qU&+(2j(v##Y8EsP{Nh)8QqI^@18|LzSFGXnnN-1v3 zi1tNsc1VZ8ICe>0<>VS+KBp(wZIdNS;Lq#7UWOF@h@0%9ruW<2WXJxrt8B8DkXzYg zBnt^So@Dd&>Ty)si=KHaww3cktq^BWogcC^+WB!`v?oQNEJSwZ0C7?-0_H_r#7^~# z<4#TT&J*eYVXeholmkRjw>m<E3JPEMYl6DvRUz{Lrh%=SM4~*CaSu2h-_6mQE)H z8Lz3N=}z6ZB|D^F_3yc;&DFwIBKF9v&AFBL*2%%%tM@uQg73=NL5)x#+u6a6bI8}( zAucAcoU=pnxe#y84gu)Hbao^lTW3c~+g09)pNUxJ>=3h$>n~>qy`-HT#&SM8aGcI( z2P0@YJmiZ^Mq){B0;Wqt7o`VB7Y zlPKLami6i-tW<>nR*)u(D49Hzdx)b+f)uNCY4cSHfKTU}|YTxgq|Q1P};v>TuyX04y+KjyLg#&rkaF6OWtdN6G?q1L>1C z31tU;shEQPIFZUf55((`NFc#KjX}Fy8mIBgElW(n&vNwe@4J7zYk9yNQ@@P{X1g_7F{5ixCL>BAn#NbN@yZRmm5%8($Yf`!GR>k16^`Y zZ%!;kV&frRL_4WTdj;-9b@@*?$bu0g2;7&cii>|bZKmX#sKNq3!s_pB^b?(|y zNB{o1oS49oFZlm5lu{DZdniU7(AblNmpy5ZpYX_jQUhXYl-SU3SAH?hGU1WoJnO$4 z2c$PH^6ZiUB?6Ij9FC?E*FjgN>@kyLBNlcT27GB-Uf-X8(V!XnpW|SD{xqQr&M*GC zrR~h~PpyP58w(g8&)3D_RU%`r{>Ab5%#H7d+pjXb_53NF^YrWWc^KVgiNE6OucAzj z0sd3}+AxdpTz{iQe9;~Bl@{?ub8PthU$clWBah++|K^MM;_!Z*MSS}8S6Rd_Vg|%s z!|tBwGb@0@@jWYOzWc18V03KQBBPXEcq8B`hC%Un>}nyF21eC2~X!c$*U8#=P3EmNt&B`Gz>Y z+AlF3&3BJ&{m*K2wom-LfVa8YXw|W{mc&K?%xxf9gTlnnT zm$~X!N5^WL%HjFf4?nM73_ma5aa(gOsq~^c`6g@bqDpiA?R}*UD}HHP#7AFqr#j75 z{h9Izrz*=&1vO592U}_FYDHk3MuQ#lY;$Gso zl5tbjDVVC~oi;Q{0Y=W}Sg`*Iw#Hz8;A3^r z2g;chtcCpzAUOUL!(QpNX5-9IP`fNR=-dP^;>!SK%fZ@~*ZBpvQ_*u;a3-ME*Zsj; zPnS-uH(y)J%NQncH&||kWBQsrT5z>W`;=TfU{?7MC8jg;Ngh_M3?1XVwx2ESXDd4m z7WVj+5ekoJ;{y^OHrB~^8lLep=jY7JH(5DZzxRo_PwO{n%}r`pv^I9S1eK9aaXuBof0P*CE7wWwW<~c0ntStExj7 zCnwr;``LY;XYO|o+asQU-q^c$@xYk6r*Q1Cs7v>_)|Q3W8i=TpeZ!N*7SE+j4o|K% zjn%Xv(qH#v)U1#9e_2v*PKLDloRt?Oc;g?)lZR#0d)?c5zfYfjl|$wwY{c!5`DTaA z1z*waka@1voCsgr|490l#zKtW4w<((WR78qClc{nA2Jv1g5f9__30*^yE@$(o*goT z^-mcI%F>Dz&A^VAwMX_ zzuiOTJo(FTeEnie=JUtb_kGBm!PE6a_-4u6ESa0TY^o=7Q3t*qGUFk0>f-ORWRkVt z+LE~#TOW>sUuenj$m7`SB~DI0{I_k%yoB|<9WvkOkhv()ZkEi=k`dBO?CHcS_?^UL ze#?i<&yQC*WYjqM{PFdDTQVu2b3o+`00%|jrY8-5 zUQFyu83UzWghM-tg(AiiWGKO_LosU?wbLF+@*FJmOR0F_drD(}{zXs6@ccYV7{^~XPZub%hv)yQdAi^H`PvN5 z>z6RH=x{`*@sLkknXA-u1@!|4$CwuMf#TIk>2wQ{T#HS|Fzw@w6s5cf+!Eg!C^bb?|WnMPKJd&>Hh%+0bgL4k!>2*B5A9j zk&ngtmND4urQufVT49A%-*1<3rK0~H8Wx>!kahp=$NxW4E)Q}#kNLFZ)q!HgqEk&?|qX^^L9xUatS|m5CSE&Vm(fSjp z#18hU%Jj6syUtkO7a82zmw;seTEU#`;WYIgb&oN=HySt*cFR(*kmj{eXd&c$L5P^n zm9H``+!iP_5c(8NJ}SYa=4S0#YJS)jU!RG~ZheD@WKQ|@pDf|xf(@1a$(k+P$kC!r z%fyV=d-|CrHBm*J41z`Td=!>vFT*f=$+2wchUhU>A4nK%MeF2nz@ZM@?N>2a#r$ncNbm~6d76m77~DvvPpifQbEWi!`H9OXe5G-FFhnxH6V4syi!vH zAQ*ftIrbUP&}Ui3cp>ebpujT5U{d{iUO4kAExpM0MZ>o0Vc_hre3S!FLxDVzvcAb4 zfuvD0B80t3N71!vn|j+j``Ov~6zq_Vyvu;BbWvwrkyVw$o{YdOE%>y9!nI^w_E_zx zpW2q{AN3Zrh3l6P-!}nHhT{DEl>+>t*a%YVCcvMIt|oAd`&Sm=msD>cvL8)=A0D=U zU6k-2U4UQoGo=Dmhe0I`0ho`DTX+z4m?Wf?yqg`r>pUNOUR)0~`c(Zdi$1X@VHIMco6X+cj zIRLJ=k-2&R75#3r!U7|0=m|o5OF2l|5RD9s9f=p>8OtuEG&i^f(n3Q3h&L?I?Dtam zal3@@g{~LfxeC!M*n}0$$?k~^mykd10&E5%zN1Ei`WuU=K~(hh6H1-1VP_mM`FoxE zPJWF>(g*aWya%PI{@yQl^uLA<8B5JRkbmk0oO63md(EdllSgJspWd8ubA3x66dEIg z#z?|-nTazbKB&qXm`)X=bl3OPRiCC8Hn3?brf+p}op;-ZC}8wQ0&J7YTFApIE6fAY z@T9D$xuAhfGdC@%zM{^53OY~vloaHwRFMAkI^)T%G28V0vUIZW4l9MZVDsBt{?sOO z_>k3KRs*E@Dx3DD{F8r-O=~gdBTsp0Z<}`7<=@Aqy{MVWbNQh*Er{sP>jV(z^>&p_ zJL35lQrNC}$s3xAx{%xvnFong6R{ch7Q;Bge>#2;pgkV$V=PBfvvS#*owLi`jUeqz z&6J{51g&IMkzQ{Vg`Pe1U^|?rjmb)fbHY z?hldB)JGzYsmJ7>edQ`S`~^jFr?8&f;?AEm7rZ&Du`^f62~}yUl9RtX7toXD_Ti?1 zPR(U2%~R*2^6c)(v0}#Jx6JLFVx%mDuyGdFc-s_i_xYY$y5wSCM_X5*T5k(mufgX z)-p*OKXR2xmTQ}!L!(UQ;#A6&%e}X>3sH@fN)9D`9HI5Al${^!J1`h84=`L{$~dqB97`?LDxpkuH9^yjmT z>Rj@o7Zb-NnjaQHOB)P})9?X(7KadP6`Gms-C9=&J57D_CXF{Qt05(>&Bat`#_?Qk zf5qHF(-|dAvT+@$QiO*>Gbt}KjX~8n>L2F5BNXv9^pMSqK&kI+XI*l*-FDV>cGksU z;ZSWx(dHj;XT3qWnaxY+kkCG>T@o^}2p4>d2N~~C2+>sH0gex4Uo7#oK`t+30Hq?b z0*I1Q5ySgX7s9RUQnll(n2KESknG+S)MO=g28p7|c51PZUXPqksj7zNfUg80!##n! zSE1!8T~ZPSae)~X2i@Iquh6$c9>I~e<-y{&5{TO%=Ao!^wg|6rWs;D2LUhEs7jvobRykwt_aA@S)9N>?cz;|W*4b`6DD*zznMMf2$L32D|I<%Q z&hfhC+f|gi7t}LwLd<(y1ZuY_KeML+R2baAg}i|;Zlg$x$A=9_c^-X8+g716uS^9O z#c?K^2!VlObdI6=+<*pR)nI-osmz5ZfLHcSE+VL>rWizAoORN+wyGhSJD19E>8eK9uXaX#Mok{xfqS zII((TqOWmP?cT3BTN|w$t`oiL~c;*WeBA8rZTe!u^ z!xgTh;-?dU4zpzGKGisBLFE+lXj=A+7oYw}J&(Q`>Mc$X+T}y47&gVJAE6iR$QbU_ zr&M@cg(hnUK35(l)K)vN0YQ#6VlRhtH9S3pS55^+-X$-FYjxEW3y4mCzDE*Q(Gd*! zaaD#%O*I_KfVqjj+J4?z-Vc2!KMz84(o}Tl&aM< zI0ja`N{wn5ht<+`m1I!r%}82**+g$c6Gxw+>v*#UpUEbB=eSc$<`MNN`Z51|mwVR? zZM{#`7QA~g?!ao(L~G;N@^Y(DsQ&@|9)-}fq;@5B_rP|cqdzju01{&32^L{53yy(7 z@UCgRPvYrV2BW6B;;20AqHXSY&|>WI5_;H%ZG`B~L^Fg^Y9RD}lyuMo10OUc72adZ zgi$q){#C_rSZJUg6`G394QCNUDy>^%(HF`^Y0pAIVi+Js{0*_)j+S*_a-TA-wPu!O zIIT#vJXhOOpB~uhqTQdk^ybx^l15M!$1Xh!VshwZ{(VTMJ!>KnthN@G%i;4L#c&q! z(8DSW;eWDz9xT+gBHus@aR3#es?Jova&JQ*-o4g%x-bd_bULC&Z*2uXRR-M3;19+enVm%~S9>D1 zqIos}s2P7suecuK<7A#Ff^pbQ`)gHj^voT^#%le!J?_qFkJsj1_ITU0wbf@ykUc(& zg1))$`;cEe!Q#R)CXp6Hw`w6pwmlbibIb5#8}!^{`qqGM0@kU;x+_wvCYwCwSTt!k zmP~WQFh=QBt=~nQc$}nz^P@CBV37lc?NksHmg+Ei1*_W?RR z4Et(78T#zz;mBD&wogXw+@zE=B`|n07@$rUOcZ6qBNtD)g|rXG-qYRA?%#ctwIv zabp9Fr}U{c$bPtYmqao-zngc^=ahAk9i=_FmF0{|45KG8rtavdY$5+-Faf8-m#<(O zc%At@9-~DonJP^**{0MmSTNaSfZE*S4>Hz;0TRpM_6MYPXM#DXA>or1^4y9Ig(jM} zTXc$n#g(3>4q9(b(0-nd;#--CPl=(3jE z@ETp6iyhd8_oo`)2WDlBFHm{HTQ$Jaa$`jX{=b#=R~FO5`XMD%`xt993<&`6w@l%o zf?*qG228Av4D*8k*F4BoE;Xav2l=NADIiM1J#%8f-11aIENp58^%2ehTOdLbZG2fh zavBY0WnYcZ@ZWdq`nIob z`|7r@{?{M=@b|y}{h$86{-^)=KY#m!w4?h!s5tt&yHEf6*T4Vo|M;Ij{_gJH|I`nd z{_(rJfBnlJfA_bK|Mx#X$nmvr|2CIUIjzFzoC#V~MGToKIOUbT=I|(Ec46MwZ8vHY zl&7?utA$NWW4CboDR`=(<4sA`ig-2SumscQv7JalG|pZu5B(sHsc^;MldZ=7&i?lK zhwvHAdFzM09~a}r!*?$I=nozH(OyU;G*N; zmOK_WCQ=@#e-&!(!HMpun&3k6DF7WnYJiDC6%guyG}zJm^B|EV%!Lla9txt1TYKLZ z`NaT8v9s_u(s;6mKt*<4Rs^Cd#gSfic%VdjrW!N}4hSRMAMO$qfPHz#J&=?h%C=e)wFL8%WLzUA0n zyoT!e_!6-Ahup*$SHNu(|0tXIvQG6QZsKDX=T$cGOBiN2)i_#`qaVRnj8V~Z!88S( z*B_lF_^dXBuuys6kutVFZbjXcw{rxO+Nn)eRX?|!8#2-<#Pik1q=biw&hfL2_)oC1 z%dd+%j?9zkNzb}e&!3(O#JhjjA3F62V|n}Vp6jDY9$!Kpe!CgIXmq^I3}2KJUwMY#0$cj{5?)AN zJ&lepa@a8RpODUN(M_O02%?>Sw&A~`NbiG*DU5Z9w+fFiky-_Iou;RNv+MU!+*&Jt z-hA)a*8WOGY42-xa_PRUan^mIeFC_mB_XAtwktr$%SxL?%pf%m4F|OsPMqQiqyPi| zM59xg5o#owF6KY(S*hq0KN-|A)ffdq!Snag`jXK=sg2_}nFs+&a2Hp{AKc2_%2yZ- z&F_NZ1}&$BYMUt6id+Z)c?|&7zh>FVmvRDk0o`z6pUTh~dj5PO;&z?}la@G5#BQr2 zHI}Y=(>xJ=-zOq}?)MJ2^qYsSA1?x?LtYo6JfQp-0zsB=aumM?k+-!x`dEZ#sfHpN zD_=DrioYcx#56%oL2%LQ15tO?CIFhhhJPn=H_scPSQEEJxR~knE%y=ATxQUB55f>Lvzni#t3vqGL(EH)U#r5-t ziyQosYgO7JHR3?s=8kiEf+I^P;{TBi&XX1VECHI+Bzb4>qeGi)ZI(hDT!6iC`|u)F zY9dHv60dTQOGH$OI%f>RK4EpD#J)8ut2v@GQ3tZppV(JS~y=0>>2$vZW9o8q48 z-l^_YN(3P1O=6YaRi37l2yWh~SSRJ2>b|MvhW7i<_anYJnuU&IJ1xJOVdBE$^jCpqiA-vI3&-y7&98lu?)$mxV2Ptu2UtQquzP*j_Mb4NsN}= zR=IaHBjH@RUh8{lRg@)QM2U60s{Eq?M5mvdJZosmpwJT%&0g-v3+7v_k9!%7zL##d z-i1+@CON$-iEZ_+Y3{1Gqo%lgLLJle!6+PHK`#e&xC?={u`x@=zlE~zFx~~dW&0IuohBn*zqSOV;9^S-Igkg-*W`>|-qqE>;p47Pr}Q`mx=#agH!)PJaJ701 zF?3OPxrw3gUJPBdll~FJ&|m%}CNiK1ui`I#2^Uadf)LNVbP$W;Y#qcV$dW298Du`71VIn)CGzm*6vnKS2l`l&qbi^uU8+vr{BrWqgwKIC zAc&dsyC_nni_~FEp_n^RFKh2=J$_=pOWO`T4!juWN>F$?w;aWBj&j$~4=HHNcNe@b zA6^O__wnw~&zy@6JdscFH7Ob1V|bm`hjm6PL1Ib)ErF&{3YdsM((!4Ny{UiqyZcYx z7v_b{7((>Oxv_WTqBtBn!fC{twZ-C*j+koa`1YM)NYvFiZqDHpjpNcZ_M{Ktvu}9* z3FCdkdKtQVeKf}QB{W8+mzH$fB-xQy-Xs^}7{l6plTC8*+!;FZ9c_|m*VjHtwl88} zfNQ~AVy;Nt!?}X!r3A1rKMyd2hx5Le0+3e=Jc&vr4VFZPKMpOK#7*|yp>PWP;4Fy3 zdq~nF0;P|Vxyha-c!~>yvmCy68&m?Jo~%^CyozUm;GoV4?{NriU^)nY4)kC;7<4wX zxBDX&=!e`37Rzqnqi2<{%aOT=g~?6bGUUTha{w<2z5$}e_d(voZ;zqH0ietN_=s4C zCm6gpiF*M##LPTAC47qyHNN?w2~v-x=5)KKjkja; z1LF|`5L*nb4=jz|1(U8*cc6YD7x%4UKVkc8fzvjGOIPZf!>%*~3Ue@L7}6B2pj~$< ze{sK^?{V*Ee`*#R01DGG9NgH3va)uZ09k7W6%^&C*3UL#I%laRh5VNd`18-WnD)S_ zGYxUpP0?E=!!Whiuu)YCfP-3It&D-K9V2g5ur_9~K^F5Zc3OW*v8nXl3Ru;ra{_J6 zl>nDS=ncK2P2lH8HkAiNkFv^!F4f|<1|isN95cQT^_6t(Y#|V5p$t>l;WvMR1GKZyj zlX2f%#$8lnhiUr0WZV}@(5=h6khGWc^Djw`?Vl~fr$16 zi(50W1CWj)49Fa(LmPHSEzMNUTkzorkT(4@g6JoK9FjtZjZjF0mg&9gsK{6&A5FC3 z#kZ0{^)>7pQ7smq_*McgTsG@ctR^h=Om4~juNqt`j-aBB2fkhL<7AOcKx{)2Hj#p9 z)Ey4}_L!&kGBLUN{e`A6y@@}immySay_}nLFEKCV9wELLsiAh?iEb&z!S#FK8(eW$s5;3=vxgU`|KdRjnQoP*5H?v6Zi-Lvgx->0M zt!MGeRUT6yki}83oNPHULsl@JcI~g}Y0lc*74Gi(VL=M&NL>!uep1%aJinwpr4HqK zMQ>>r(;?;rPgYhP=?<~^2oId54)vjD?LMV=`WHrJqwK7MmBsXhfKHS~lkgQ?hk8Ou zf=`_r+7sftV9jsVpZuLy(7?uZ$CV_`5>1WF+yR?K3ImlbpMpBZc(4dB)CZG-aB^y+uR~%B?ynC z{$6HukQd2PaPBxL!VUW|VX;SNl(u#?TjVH8-<3g*SCuXAJOcuJsKN!j9r5YM3KyQ5 zD#N0RnOHuB{J|pE-T7v)GK8J48b%VB$(NOAn29aFrCa zXxBUi92yVuCCqH>ZH)nQT)D8~c0>Z6t6<0cyDdLau`m_gmR~4e{^g(j4{^f93>$$j_oEP zBZ9uUd7pb|FDgWD>nTb6WOmUW1yGzvXCf$zhj%fR-au2@y#$2&O~{Ii2(+85cmt2o zx0V$bVMsrktat-F;O=ESAaD_HBAJJ)6k(LSx{rl3lDW>}$T81T{5n1ZC$ z`d*AT@yB@5WLZg>$M5-(dXhQ{3^Ju)w}+2$?p1?}YH?d01(u%Mf>)E^J>I68hhB`g zrKN6DI3QR(^r*75R)>e&mf9kD8!D^Ex~&@WwzSl3vhaj_-4-$dus^rKAG!&R|8AR` z@}K@y|NTBfB&tc^?y-}Yb_|z{0DmBp$D=uh6D3W#zXKN^L^uvvg)e6P3?d5ytXmU6 z=a&tHjbnpD(h-;vZ3vv&pJfI!p;ExNH$6?(BeWq3LGVVrT7^7h7^ch~M<)(1yjv_L z#v(YI0sKkwJ7isS#Lx@~rpCp}#`R+nd2_Zuzy*dl1ZS!OfXOgdzBkcs!yG7(ZgLUERL))Lw=xdHg8 zq#Y;-z~YT}aY9mh9X->id9nPzOl^>!%;khfJq!Dq7e;N6#&EC*S*y&*$-Y$>WG8Lk zY;YNGo_rVM-7G^30+E(!o9ZetD`hvVk-gqADg%Qs*UVjYFgE)qs7e?K2DtB1i36=K zi(kHa!8%#+ckFjdJiKx83-$m5)jy9oK9wPB=_8S|7-LgY6=Ybj^E1UV7X|va z>r_KA1g1zXf*H-201m+D`A_p89B$N@yxZDq_FvLpjT-KP09EZYUni#^OvBPR;!6lk znIVBJMCz603FA7UFDz@=z_=q))<@S9Zjux^d8<|N4At&Lg8(iNZ*^$Dk`}q50tDu>2uSRv(Qvl&M|k-UKYslxYddJ^V&J}=AUlpbbomZ_8R#6fx$U{a+C|`{q zVuC9ui7vM6$B^nmC<7t?6#oKF=>11tA8v-wquGWW6SjJ$O)w)8q1fJxT4ELST0hEG zCweM=56+NECboY$l_rVEdE;S`h_z_#JbZ$Y8WzX?@R2C62I!fxG0}?b4Nq@MhUVPX zZqtXlubvH#U2&{%R{Ox+N58VLs$$Rjkn*Ul!3BYIzc2Y0-fP{A*z~FEx~=sIBHe6^ zZ%a$v7Oo6yFk%y-Pzh)Cr~LwRa<8HQ`Q+FXIZOu zWYBF?8Ai64%qjWaku!uCI`1^K1)bE`TG8*FY6K<(40M!MCbxRB#Jp5 z$7F6uWlW(ij!pY6-b*1eA8ZNM3T+GY+%f*Td)x4 zr8XVeTKbuNyo&Oi@j$NU)(V>bz$`Yp`$Fi4m7V;yLpgTkoJSPpq#`(SK+hsEkR>OS z1j>0U4DUYGr1H{I)|dsQ6mE9hPqp$UP*+;p#- z?iGuvl(Mg*dtIM_cK1^L1lv#WJDPax9?1_P2w(2@#G(*ZOtPnW2CP5dp-rd7QTX{+|EM*oll{T zH)!+WrH{SnrXN0b7|P}O{p#qQ?e%>y-y)%U_Yxrf8!n`aior0p|1b;bGG21oHNR>h zy@fUS62iG$&U}KCOsW(sjCVY(sAfq7o0dRe(FPvI^CHiQb3l!WLM_MM_W`_vaWiup zBvm<-v{fbyZaJAxsdAQt@MI|=ER%@EbI20s&hZ#fD5%h>J@nDC|YO!E(L-$?ck-vGQAi3b}7Z(P+Zpo_VJLLVTjE>7|Ia(Ssuk)Z~W@w>F~ zx~~U$_4L=>x$2TuG1fQU&w>?mAi_zR0Ptd&WBS62%i&1W26D{39%ey|I^L#N=-M35 z>IU=##tf{UlY&Bhb*l8cpZm0|CSev zJp|KjiNIrnsG)1?4ecVajrsRRWBL{N+;p3G=1{29Y$#Rc#Kh(XEqJsH@qg>U!r^K2iy3 zghmRzl#`p>!005+(7ag$2ApW%GnK$twPw14? zlH5o5fD{U;P(CnDt))*`%id*5$t)gS%Ec-llVC|X>l1wA(u?@BTHj@1S7mt8hUj{b zXnzEjg9-w0nhvK#nQwllti~7q&WlgZl zR!kazu#|2WyCRJV5R(5jZbu$|UrCPaEhQZ!MU8eQqluO#Sw3;;i;m!vXla69o?)Tp zcO*ihQ^A|ck&H~zWc?Bn7TqBT*NlH0_m)_f;j;*TGK69)_Bw4avh6{ttazFjV=Y1W z7h%V?EZeHgI&x$PZzKefWL7dlR}u`gigK(7kqPA_Ylr_e%SeEa9Oku7!l)LgR3i}H zFH~I{j^yklv7e-6mV`}StKTJ+L;P)&rq`#_*}asNa67cGmTL4J9oiRtBez3)I>>K_ z_U+IXVgFtZ?TfbJ?a-c7$!~gSzeVMm-Iz&qO6t6#@Nkg?-@_b^sgt@b3||U%;`LEu zLSzv@0F@HC1<1kUV?xY;Eve^-cZ1HVxCjUD7$bZF*7Ht&FsZSrb|XnBfs8T6fTW+ORU|f0D6`9P$*_L-gzeGbB@if-($=sDL?OPe8LmAu4zQY(oQ-5H47yt-QJO*|>k}qk*>~tjiN^+;M@5k2+Gb07i>%y7 zA$)2R-wQMcfW79gn4Bo7>OA4FLzKl$oOl^uOSMhmvMF)~)Tc}b?~9X=*W%*h>12HA zEa*37p^nxS^xm_zDQI`Z6<%vIjs5Ybp`fdb`Ab-~?{Umu5C~sm%-=*FycEOcyOgfL z51V)d7g|vIoPT_1JfhsHIO@5XdHQf6+YJ;!dXvgO;b#slv!9I>UgEah^Qu-ReX`Um z+_0^XG38hVrYMkhOc<0`@7l%WvRokcUb;6^@MLEP4d5zFuU-{M`}r@kO(Vn_x-{@ZKXTSTw< zENS#d$476B&Po)dr(#;O3q83N3XPZVJ=JpJi3VwhXZ5?p#rZ;4OHtX=-Pboa6g!;% zHem3aS9Ige(aDqfn1b_qks(hBgqj0h;(5qxpfd?)#vU{|$s|1EWwj*u!-tv_%L3nX zgu{Oi_a(sx9fBz%byC>dCkpf7kcOPkkVuS3QE-|xgGW>N0#HULbpx~AR(Q~Wo4dHZ zA(wf!Ve)Cx!cvKKAyc`H_$i@BGG5`*OVy?u;M3IF=iDrMj@A(ligGsm(EC}c1WPR;p zF~1X)0iG}Vq;~oWP0zFSnO$2JS^MoUSm-tX1aW;ulA`@fV33lGQWkke!_u&P&>B zvKWGQOcFFdT3E0XjZ<*MF#vkM2|*-~PFZ@%#WIKKjpo zq>dk6>(FBOK^tw49e>rh-ACIC`IF6KDEP!tolWAi z-=*kEvNRP7?h0eybF+Br3n4&nahu8s*9j1xBq%4=fEaBuwxEsZ^Iix_9WW-tmsTr8 z#3+~Dng~%czc-%(I${K@i*$`~c7Pw3Q|@OEdlp~2AsD^7u{Auk-zG#f|E=BC8|FWV z4;dcWmFqrM`H#Xnf{D@od92`h1VtEB^zQ%-L46o=+y+NQ16UQnfkkC)GUk(^&Em~G za#JMWmLk9y@8`xT{8K|)bKAdv^x?u&dbaWgLywO04*G;kxMek`Uh$L?Fim)77hm2q z;fP#)E{k6BJ--PX6&oMxIz{xR0mz{E{wLNR1Rls72(AQu)qV9HgeFD4+jmIhr8OtF8hqwbCf-XctkdJlAoiha$=gU62 zU7()MHRQGtIu(Rjk}d=rh6y76^HHNvILXzVcam)1zz*i2eu;xWo?k^RxOkL2lXds^Xl0`Y%Dj=3L=D^@im|BFK#uB34djG3h|63}YG9^nQVmYN z=eZhq>chFp@%~ciZv66QU%dGm#=sSO_*7Y|1OB##OUvy${-XRIXR^$Vw_3yKYkpo| znGyRi20MP=J&j#8m}v4+{?u+1CX<{YZ7l{mH(TX5$p<0aX#Y5o*j6lPVlN3PRV8d| zmrv~EXt+f9s+d`VzjE4=tH%lnx45t!vCXuXN`hD&k`GIp5ad*ujEG2BQph{`h;L7<4+6~|n~%kzwVOtrkoU#CK9EaHj$T&7#mknQ0(mzL@?{70G9Vv=(e z6!k_bKFm9zqsazB*~B+cCS@;awmm$~w1PEWbwEHJN!O8p$5xN=W#p1UL$--+sV6LS z$fm@H9xx*VOielFq?*c1bA?Zml9=N{rccTp>zO-MdjF%vx|Eht4X3-(q zx@R_8pX(?Q;*W)y33R-Od=_aU5_vxq-NhP~e7NF3R2D`fvG3r_BAkWPRRr~s|G3+* zw?%I7Z;RaK`iEHLW31sBkeD|Lb6WRy?x5I9wAgz|OU*7+Q;9H6!ZZOCL=Mm4AmX4% z!s_@qluL9M8SeI!isIRw3Q`sBAd>MuQ1Hp?mLFA<`1w?jil->IQ|Ia@9H~>ONGZTY zpc+@^Hlh~VWHZTQIE+ai=T(905ReqcdM@c%ziq-8LwEH`?fCjS%iK}pCnWd8`O1Re z3Faws*)p*>`GR(Q)-P)x5`iPCgQM6`6+eay(m8A7y?y(%DU1N zoS}hI23WoBz>crD zxoo-F^)LkhQs^>XVvn35`%|4Mz@@9>(xs=0 zyw+}Y+iIh4=(PaDXmj?FVRBX-IfGP4m;A?xAN%^;h1}+sxeKdc)lFplmPAI^j7|;4 z--5_^jl1?c7a4CrH7^_EvdE~i6!54q(%JztSyH2boGO+dOUU8$Sma{Wph0S~8PBG6 zUQHFtrQ0iXvQrfFH0(ZMbP3CH)BuIb5}Hh^or9`MmW!EU?qRn@Evu5{ zP(Wa`DOoN6d6;UJCxBT+%Mrb!*``KF6AW*<)76CtW}$;Ot!R0vJ@?;l4I6>?B4y{^ zor{dqvgm@?bHu8TbinZA&*yB8RwD|t&S;Jjl@c}rxD;N9<`C3dvK9;~5^en{&ozsS zeqmSw_Pp4+`JctaqA5G#`?8+a4_DK|hnd(RW zMXfll&(xver>-M^3I?*&H`ubKV4%zqE`~zP7k3}zJgtWF0-%V3pgzR(vQIGfAfh{3=B$;v?b0l^+&u z%phu?m?B?rZLhBbT! zW>2Q1zDF{dOX{L+j5E*vRc;4CN*EeRi6nB`!*Ta<4{s8HQvD}caJ{Kt0YtW&?&jZ3 zFu>d?U$H%-Uwt5o05C~-LXnu-OWF$;BdIF925y)rB=P_&$7Cq0P*B63->%^&B;j$%VSay~pu8A$A{E zLV6A-&v3rR?GC#ue>aDdt|)AkJUUG@o#5zOU&Pp zZ{YC|C;Tm~p^JE_Z@7jo`do&6^UK%J8{n~I8ojWF0&}#tduoRA5mO;Un4ZGCvOC$9 z$(2g!$_ewz{_H1s(Icmr;!5^CwM97kr)yR zDiQu*1w})J4QilwkYaQsI8R2XZ~>rL9!g!>A$^WhTD8!3qz_UOoXXQTAUk=7#6lia z%43GGE#wB3spDHDc6OjBeTULCWn#r_o>H-BPszYqa=7LOR`2}dXj5}zncH>W&@}&T z#vLaGs}I)29Wl(?+11xaD}LfiW-~)iLCJ=!o%tS^EJ~{y1n>$G0JS6RF-MK)C{ zAiucKj^$lEWDu3>kAN`HB#6!5QSzWUN3KOh;1zwBgC3cgmUh*fW^KXGo3@Z2PU?bNhdcVc z!;ySmPm0@r%>%?I#PJOTzoGerwu>6~|F?H<%aJ6Y!l-Wz5V(K1$)G>u?!) z@2f6 z;VEYlin#*Hp9V1)@JTU0hyOhrCClRZ!@t~C+et_of`d62#I#Pq3o1N9gyXTV(ct@w zxL4==JU{r%yYrcoC*}`6^DE#pCpEHP8J~G~Xg0~?j2%doEe@Q-#}$5_QKXfG;%2(ORi`SB|8d9S?j`c{e;k^F)n@N*u_W zoTAB#b8~h3DVPKW0LTwSmOJV92r=MwQp^PCpvhYt#XL)QymKmH=}fZtZ!Ax}F>Wd^ zgv(FyLK!Zd^$z8jwI$dQF|k~%O^^L{-5ej?=ba;2^)PcrHjoQ6 zAc-h>$)wuGB{ainy9{hPOi$=+fG(8CTDKgg4)^<>rwnd%Up-^Qx{a@{4+?msPeLC7 z`KC`ozaY#lbCxyit(I_kc5dsxvXy16O#9=0Ot3 z2&vQZLZ<8<(F!M~%7hUxr9>6JBAf&X1Y=nzc_La?Ivj0D!x#kLqJrk#jST37KU4Z6y?*wiZw(C}<%+TDPNufw2RM5V)n(CCtF_g<*9hP`11Db z6j2JCyt_4~^`ylzw=SO1VW-&4T+V5A6zX|gSppEQLCnx7Py%uzVJX{$3$0CJ4> zJHE_!o&IUgeTht=2sq%@1z}63T8SdcZFZ{+7r2(7)LgA?@Nmlm5ue8I5n1c$xo!Lm z>>OknDSs`pbDE_end7O?&ttio=TcR)#d9gFdGTBVMmO^mX@tVC&h~lV<%bxM&w)37 zY|O80%#)s?9~<+VZOoI$_OUtsn2q@X1@Z*bojEbtNhqM47jzux2dUTYZ*ojYr=uhV zbxni;Z5_9StPv*&t6N2AXluePln^X42q@i+2}=~AI0+#Mo`mR)PeIBYg~=#RA~vY? zF|SB5%#*-Bz~4z?0`3(R0mu>rG&??92=7r`D!WSR6O^BFXV*MRASSnk&Gj;MxD&`J zEQMA65?&9iMJBY})nkqzYGg1TkUSv9^7~Mr)y|2lCVjk+Y}E&o>Mr?fasEQg+==PH zU*pOUV}$3X_nvo~PXiV6@rZAvPba&r@hXzFPsj%+0p|xBKT`%`x^i1A6&Y=CF;d@F zZlz_$hLZSvIjMVBXeDjn+))(;-Mbma_dcf6l3^bFS|ARvaY$6ib0d%){w4vrl9TP)UwxHfTi_8 zj``hC_XjwZ&SIxldi=rW{()@nv~0XC(8Msl6E=6!wt1X6ue+!3tnY-)ZM`nv3!6JR zyT3v=J|)!OgiVVz&+gmF(h` z2vm5_!^AF;8ify!L2$(5BKcTyPcg|?MX^@cEe9MK??UP&F|A2(2)RxugJkn{SeiJ& za>W3!`y3TeTySEF2_%VK-IHGGOEQ&JW=rUmIEkDtx*<15gf#)cQF&o6lB$Fsh9GlG zU|ZXkvUkir@!dqcA)#{55y1A5GRG7BCC=LZ*`$NW7=dH16VqEmRNMwQM=BCn#AS#- zXN;1IeX0@i{p6r$W9ObX=F`-}Ky ze3NB@-(N8BePZ|;7ALoR>)oRwyO&@mfRPb9;=;pKf*m3!C4v}idtHHL0a$L9FiAuFqW1O`Nm|Jg<2pS+#yGvzCzX z6j4Oy{feO27qPBt6(c7B2Bps7SjQr>%~qO(!4po=X(FqG77nYF9n+dmCcGuGKm=LRdGNYO-`USX~tka1%m&J74 zz1$qK>z^APKgr%XCw*0!MKnmAxeL?@$*teZ&ZNCEgY^%td%1sc?BvG}p@zP#aI9>r zOzBOMl>gkPG$mo63j${BbGmlAH%)W1bqihL2gRkAjq&fAzH+jx}Rjy>q z-oq9S_prRlHT|k!Zpa(n1z-w$$LoA3|=`_>7#O5;Pm_yQYK?CHx^o3 zY&@TmwNd$`q>Ga5;#z2Xbm6+Nyndu~-hSEx&$0{UeC|zo9CT*4e|9`4uw0N8PDQVN z#vk?|FPmVi-eylOccpA?Jy)j>DFSfwL98|`XLM!8tHgUdc9k(!ayWd)reQY;^AIDN zXY7xXPtl{#azn&PCjdiu&-d}KVG(&1S4~@_9 zJ{x;tr>2Lm0$ULrAEeE4ynI>P*2mYrok{{Rk>~vWkkW$W8Sfu$N8f+1K=Y@$h?=I)DVNJ5$*^%PSnL_kRz)Wvle`Qq8#PY#d?x<0ocw# zMb_l&%$nprd4hA~2$7_}l+($?TuJZRXA++oOLqH8_0};m2Jo230kTlAiEe^?S9y92 zxCn9=c>UZsreW!H=5Q?gNtZ7WBd(APhClnJfn9DQ;kDgMadbb~f^%QBeV(2TVKeb> zP8Xa^UG3{lTA|)HI(E_)G`$=~Oin>MPRS40FN0~k<;;(gFPl&uVlkFXniS4nfOqyy zGn!CAWcN4(oLf8rrZndJ8gtSyk0;``sJGaIh^UsO@9>4Nv`qup{b~1aN4O6Mh80h; zZQ%t8SmTBM5~faObGn&DPWj90FR~KsO&>g+G)-8+je0!M*PSS-1QEeqPGcQR?Z&tl zO1})?Eb$qaRbrm=+41lhAT;@?qZc3fWL}yKj)I0E5mWs!;tK`P=}o(0fUbAe4vL*1 zuYB%(n!CC5d`!V<;oi6QCRLl&Woa4tvvY9GxL$E&cioN=WUkoLfg;{k+`f z6OPwtVSO5X14)(+OiO3PURZ`6wh>RG+D1!`V`ZXDzNr-3*w>PgP!mgZM=9i(Pcm-| z1+#Uk2RJCF*hFPZ3$oNRbulad4B;Vp(zC?KIbzXDmCFO`f{asRC7h}6TN~7|zR%M( z=)2Eb#-eTqIc^!ti=m93^^r^i$vEh=sJ9Pf+|8^|QxS>z<&1C3(vMyzGsr?mC9)=3 z{Y#2sw2gCYD=joSZ;3ZPI!)#YuiQik)4SJo8U0$D!aQr+PjBBY)q5xjyK~_-tX|3K zmlHs`e4bs@$gqWur%G)Rh+qm-S{?tXkQC#5itW&tZWn87wPPTQ_B=)$RHg(uI4<6RR*6zn}F@Hja<3e6T zEWyUW^Ub$Y)gmMUAcueKjN^%EyrXRp)#+1JPh}BH^zxVyhm!MkCwbs=_HerM$f=EJ zEkBzTrGd@0-xI0jIy1SpBtO?3Nf~C~llSv5C;cxOhHN<@#V9k_K#+& z>-lE1JTeMG*UMqFr!w@luh-O_Kdth>yX@z4VJ*ph=9G`tHkO#yPPa`Gju5>K-)>-3 zO3XQX?r7>4V>z;|9g>Lf(vqWTYO0o)$uEP1ST5G4N-3A{*UBhYcO|17`G*YK@9r1g zjsPSl&tXTRlR`R{f=JpjL6x8;07feG#CTGMMd*;`F_s{y80ewi34jL20k4j5_=6%J zvBGD0^_0*~yuJJKz)F72wg9EUIYGaR$vfrRN;yu zk~RdECd$|6K~k8?B>}&!mP*HTEpB48B$$PFU0kur>oadKru!D&FcZq`n?U7h=GVrR zmQHHUbl7w{_V>=S9f}fzxa|; zDKQpMf%+a9-8!8gD5f9KpZO@!t!3)dt-O?Ai6`K}g%#yU5O2+#%w%dKTg~3m`?7Yb z_XMbQ>2OhJ!qSyMi$A3-?)(ZUC7zW&;&J~5j<7YmzRN~E7n4yNHTL`$8Jw!rm2#hrmYHiC4YPB|^=WD|SLm$X^mTGBAV0gc zdE|=1GqIwwqbnRoN)IO>-dh#6Ah@zt->8ODC*9!)m+5<-=`J3f)$#guHTp`;O?|1> zW8d12xmzeL<)DhbQVXCiM$2Qr*O$hR#oI!mYfHb%7R6ARWm1gPOwogpJXY~QYmXkA zbyh>zEH8^XmgeNaKkc16bgBCK73%=w9*2N>h&RujReTvOmkDuvL6R}`?zc2Km_=J_ zF?&%LUoQ5zZ(8lqHVwf!@Xo2fcXK$zk(9G0qsVe%G>pKV=Z~p!sKA0p>*riT-)l3J9zo=jl7uOG1j>(!aCZzVuME=F;$a8s{Vcg4wG1Azxj_7=myptuAEUo+v_1CweQq?`;Qez`+qm3LZ0|4Wu77du%H z-GR|R|IXxf4nfUgot(y`UO}x^NSyfH6P{d%P|LkuJ~4&xG2wvA#nXD%xccwGK292L znFkXNTp}qETI`8Jd(>3I5L^g99P9{PGx$#*qIQ-iBidFO->Z;H%x^Y3Hxt=;bu_Ek?%VkmXD1=pEf8O z3Rp-%wtH`IX}d$J8`vYX-QJd4*oW?w1Y+A=+s3yd&*U-K6wnlRM#%+3Ub;dB=wBLb zk8BQP4oU@)Ch`vRLSQnt%(=U@X3nT~!&uzw(dC4dY=D{F4#r4rM7Dh=q?w5~qF;?B z3fppj#x#DeXA5pMwe??f(DtWNLy`v~IWGIpQxAJnYDh||gAdeEUY)5bKn>BgbZ>NO z9#H%e_YW-bNShPcQ6B4Ntkxxqb+VZI-rL49pSM!5t2%d61&9ZT9x^7A>2N6o6#88W zFG!I?%EKI9klwm1G!k=mWXljjkL;QC!7&8l-=FsX`p%5!9HyBlYQysdHO2#Ib%*hU zwQv?t5Bt@^WgddOYi!14@uIq~-}Xwz!xxiNvbM$>7!QS%&R{_kj(Es;{7A~kwo`ke z_|Ec`7knpoc4AtW|6uj&TieB7KOerga0N_mTFn!d-m2mleuy9s_|A1R-48BNY$wd5 zvzQwLnfpckey9?*2ia{~B(4bL&Z3(sM_W78b$pl|(YWA>)Tb8`Pno)dmG zJu;X|=Q;J2!Pv&nr+7{n;UUkdPxQXLk>?=ABH;j!*aBtcKGe)p~qtyrV9>;+1Ugdy}`|0g3%DPrW0D-gwAN& zRYZ5l`U=l;gzQ$4=~p6aor6=R<|yn@v=4wYKms*l@F+fl%PLY1%A7P#yC+Kom=yLy zoB`4o97;I{2#HfBxPQsV+xgGM9wE(-WH|Z}j2Cke2;ay;;s*gfozDIMnB7_H1&Pq-iT1fO-QC{;besIO(v)WXk6E80JeevfX4v|7JM zw`0tZYl0Rzx*fkeGrN9!1K@w4ilg7@HotM0fA4oJlOTw)a>gadWLtQ=-u22Kk-qn? zJ1n?9!#aSr9l@|B8B6QHrJUa|-vB_4bXV1AFSznqEqMFH*Og!vG%K-8M`vQk1kz(%IS+RR_<;iHtOTbE zTdq8Jst+i~%vK2oN)Zlms04F-E;8vlFWQDY_w8iTU&5F}_Qyoh*p0p`CHVR*rZiwM z*rC#QT?%=N43LVEaS&ex>&==SPg^p;N$L%rogNv#uI%rmD4=Bq#($+{{G(d-=C*}$!04ON35i6 z4opFOuQ<5K@^Y%(3tGN|Q-VYoE&al0Jt2ROQ7?BTvuZsSd`x@3Up!84?`?_tZf{>( z`c)>aKZ4NR$Gn5~`v=C=+P0o(e;-r-R<5mAGWCy<=vNc-%+!yXm}k#c`1}i={?f!e zSz5MFd|jq*C+vYt|H~~c-;AfzZfDn9PJuKv$5yjs_mkm3rq77_?OmpilCqWQk2|(j znSPy@Z!6PhYGY^qB{IEdegIT-6TQv%0o<#^!Az7vrS{F$6a4k~#a?v?O}<$){! zHU^8va-u+axm|Gils#wks8At+JPaf%|H3sF2rEiFt4&bC6Mqfs6bL$dpky-4}z;+ z)a}L#qu##vXf|atlYXHt=tmc^-aydbrYy`Iv~?RfF@OkY;3>LwP0%1$CPqqR3!*A- zcLfB4kaGM5H^QBF7wm29JaD`Ln#K(bd!Q*{Y@#zATRIDP8?AD=avuQi4&v9ri?fho z2_S2m>MWN`V2J|^tSj+!d@2Q6O0ohcqO9vqwIfgTPfQ=n7eEkwtKWaoKd?4E<~ssj zbEN0}PFz&vlK_U`y-SBa^*09tm73>6%DPe>_iB?X~={p!4E)g zLDVN*`i=Z}z?@l;_(ixrY!=4BP?&K_o2~&OBH%>8%HX9nNb#2xD*+aOr--V$OfY24 z`8|4<#Kf~@&*K+iw1gI2T0Esi-R%akK<@5@CHIE7?1?lr2cw$#+rOJ%0G`$u!t(Vz+uj zT}WwyRNQ{Y^ctXH-4rGuC?Hu_Ufd1?w|wO}xzuFxxLv;sw2J+dFEn;-IQf7na3V+t zSnsKAT!vEVCeZAGzo8|M#3~De9zca@0+86V@0vIO@M~sc21e_0C46s09~`-N`xi`wNpSSTFA1+gbA0O?#fICL9b&WX&e$8HB?CvgCFWN z(3^?~m`*^=a5sG99-PNm>?~rR4`=L+2s{E{er?}RRM6IZA_A7;aEnd^yo3Vm5kY4- zQc#L~WsO5Y8PbJklL)}yN+Tfl{^&wJk2wrLA!0jD4kG~YP>%q9bX-&!SWN3;CsTBt zL^DCagfrghLXkC-`(3IYv~7T@YiH<@fXOHZx+J9@i@mc3>^u#iDiE%HvNdGCbq-p` z&sbx2=hx@3=OU*eas@z!k&3dGY%^61PvO;$Vo7empT-lf66<+B1SQ8h zY##}ahCnH^g%c&AhUQ;nw{s>(@;#>oJ)-@T@Fz}ztNmx;1xYU&NG`4%dtJ6Y;zjD{ zB1wX7E-VfvQasS1vG=#gst0R~Qqf!?SnC0c)Sk$CI-RV&b*yLdQ$6IOo?7&=;S4<~ zp?B=rd0%J+zsBj8bVd;o3}O+`ofZ)hMpM9)L=;g@0aHqCg{I@lSt6${5_|{>iAg5x zh4^>9drI70$^yGjScql1N|p&&Avg0Gn@-{nridefvO1b*uJ>j@lRg=^ebAf8#CD=@ z7I-vZe6Yi~9wLfVaxFc<7MyQ53pvN<@$vZIZ4c7Hb(T5nmh4lOQZ>D+Z~MUu=7`&2Q} zK&`Ly29qIL7(6M-swZcNApgix7NIhpL~C{;BzljjFm5_&Y-EPG*+)xG>=523GFL(sErHeMw>{1fxS3mSpdXs?`w5O04ASwB-y|fQZu^+rG4V#-78hw@&=+ z1dz(nX)PU75xPQ1vKFA#BsW5=p-T(3yVGP~Wk|)s15ZU#W|UhZ(d&_Pq6=UXazezu z4*emYm`TGo_6bpxc$g?E$iS?{3Tg+5W|jZS?!hC>Yj#YI5XrEBLXzt)8ncs;n4A+t zSm^cjQ&n}jt&sYN-jQMBOeKLQhSE+IX;i`v$a?}#bRoyES+hHYj+q{UwoOokoUCbW zPZGW}_JyCdDE{y>-;Bh&knSMig>vV=B|eH45NZ!mU*w_%!nklSJHj_DZXfG&()2>A zB=!JyS|7Avdnb|Ky4%ck+iZjz8)jF<6e+>95Qj5{t~5ZHfwLj$mfNqiIlHD4kMg-LdnLlt1dx~D; zIfuT=^>=CO^pK#mB8bmOCyXf^r*gVjS>%_ar){UPn}}R8B7`q{fa0W1lJ$~JnZuGL z@Jaw`S_u41jjZmqzAw%&=i5^hq^PrDUgpt8ue4rP>*b+T=a{KDaVXVMHdZ|s+f_e8 z86g{lTY0lgmkrOh97T=@OkHf;81IVUh&?C?WmYTFLqPDLpoCssKfW*U^?D9#`S-v6 zdvLaEP5`NL9EKs3NH8cPoTTGe?K0(KZ#-@xllh#a)Hd>$Suwj=g7Ou zE&3HH8uw7~&$fyVO(c$R;{a9TI*R>9Zppx#+~VZ2o1=$GuCN2SFs;%HSrGT^X67ZW zO_-Y)P(p4N$AUf8F^KC0TP6o_&fU?}PDX8Ec@{BnWA9~UFS%yra5`x7^;f&7cUS+o z{3DWu+X5utL&PT!7&XPe(ap2X@~^*39mwC=ZTUOOU~$`ez-!wRxrjiX&ai$wG_k85 zN=u#^lyqLmYRShAg{)!>gD*tLlBysY*D7PFMX;5zios0$<%47_p`wRE7K=})<+zX~ zJ6PuJl#taneuOOgpWtdj7VigaV~hq6JrS~)q1lMHLY65%cHEvMr4Xp4isn&^w^CBk zsGk~p^{vbjYJ6sclFna#<`F(Wz)VjI>CbN-e0p z6n?rfLs&%OR6Z$FRp)+#>OMp;n@7#)xl*B=rcuztu@MF+%p9?~;^ zu3i%zkaxT(Iv}!P@x2ms-L#DElPik2H~kNU4X+FPDFz6q|0o%@ThQX z6h~Yyi4oE`JT0M#EjoK}sCoiT4#lGEVj(=zMEnuUQ`lv4G_ZL#c6rYq01>h9I(HYV zOBWh@XQ9`TGRo$|kc~|=9Utzju^#Xhf^T$;{!G4`hni^0tx#H8-hT?p)QxJh+j|W# za77j7Vqqen-k&n7(baJOA+VNRjgurvuKsSjn(RzvR(q>(x-qMza$juCxIN1_h2oc* z)f^I$vrm}Sl2#Un&sU_-e|ogD#Bc5zIvl3~TbOg3ful4FHzniWV@224IkeXKvy zDO*&s(N3)scRwecdaI+O3~O(aPQQ^|jU5siO$Zgy$nX@!J0@icjzBA$jXg>|xKhjn6@9HGu^M!Awt%M-)` z9bt6HqTNXILZ)E2De<)48ZK9_%|*S}MSHOe*o4y2>($jgd9SnEBb`P&h{?C9_PSZF zCI7~68V2j(^fu0o^foeY$t6R2s0VzeJ(2IxI*Z}9hticiRg69!ZU@zM0{Qr?-*(1G zLRz1$Ln&W;amKWBDL<`ejHd<-pO=eCKNhm&;ur*DVCkm*jas$n=&fy1je~A2jE)@x57m*S80gL?I#RG6cIw|nu-{as( zMq*%$p^C2DoHB-PS^F`?CCEWA5J2gupIrTly*1bZPj-l&@c?EjvLHnWm@fF6>>^OD zIb+TTxPi~*KT1VC=HX=GiW~Tn9v7!B5{N0+n+$7=)FevL zM9Ph{h;XQ=DSU9Wt#3Sx;d~d^-?`i^ft@aip)y-IVqkyWRUVmX<1~MLwy2YHWhdAt z!3}?pb9FY`1Jw3CjBZg@?D9NEPs4s~FHqusy?Jg z)p$;>mkK*@P6;{rJr17n3~nzV#E|Q)4CV_jWsdTY5{mQ*f}0HuTir!hYp>t%3$j_*Ar8n|uoq}f2Rv?Ovzve5X6 z-0s}R_#>x?YiPj=9^~sck0f?)e`tyJnK;tF2zxMMj3GdEg;TA-PCk8~G7KgQk6pJDU$XcK*3ID z3C)yD;x!ye4*ov`A2*@oa^Oo#8^2!UktKbrW1e&U;YO%MkXYc&U#f_K3enG1e1i{i z+CFoYGww~tqF|kh%eo6<6$<1c2w*Ctt}()~LZ==vF-hs=kXbSk(_hq4$kO>j2rc7E zm|gj~5Kt9A{qg7VnTgMBzYQ^nbg-BTFIVCx z2|;sS04*fKr(K%msddcq$uKl|^@Zg~NMM$I09>2%H54f{$37cQ_LMVg>z@9^rqvp%8-0#Sm9P=grXOtb-aCGcHeqI{xOWE#l(*Lk23{jdM|=Zk;) z%hS^ze>wz208E3Ma8766riz;tep>YslfO|>UY{HKrS;F_+qrIuP^RdVO_kV~Z;#;1m>eT9{-P2;Lu_w5;H!_uS0P0!dE zb9?_xyVAHxk5D`d8W8%SG8!MIFr|c4#^s7_5uvt(e&@os`y*@4TdbpOX6{_wZI{q3LrcKuI({Xc*HgHnH&fB4VmKmT;` z{Fi_I+yDOS|NY}n7nlAc$h80a>EbVc`r}W3e)@m^@r5Po4C$78TTpT1y1y~?{m1_K zv48&ak3W6+umAdwzi0oP#c={8ki!JnHU0EKW+h@vSHc|fAFiN+O+{Kdp`b<;`$s9L z>`ux1+9&6iYpA5 z1xbD&q!CX}hRgnT5?sC*p!{dn)yjy_47PLh#|u+cw+p?r2F2zixn}?P_;!6-Aj?I* z3?|I%+lTtp7m^`nS=%HYXln`<(H6DOtRDAeU7ND7ng5M}ryGJM`%X9XtWCTWePOu} z+Pd~-?QaFpX^YysH+M9viZJ4=>C0OAGLr9$y}U1Mn`d}HWYZgmJa%YV8gcdPr{uPT zCyU+D2{}NXVz+9Ixex<)^(3HJ1Uoo-zj*3DWi8uH{6PA`F%=gQJJS9(X}EYAP;zL) zeM|Z2dWjHH^s>D7vqA>kTlWEWs6;DHf`{PAkm4x=-?f&@72#M41w9casm!bhM0soY zCj|X+waCu_EZyI__I5n3YvWCvWbIW+L(|@sT&?Y8ab&f(jZRM8<)ahiBsSErQm`;t z0NcD}x{`ac<}HP!C0P*7Hzp+WAsNq2?vYVXp1FBy1y0=^ncAm=KIFEswkOX?Sul|H zZ3RAuF7FxCpT-WDxZAQ=#Szlmm_}=jHh2QitlyG!U^?8K0Yg1ePz?w$ux=by{ zOds;N`YdiHsaX;=A&E!w-KlGFq_6dbpfvgi`Z6VbQLWszt2Lp=c2yRHFR|#Ytr2aq z54)%~t901cgNeWSm&-sD7B}3xm}}4Bd(^(QPR&Co#m5e)(Vi*y!VxUb5l=(z8uy>J zLl15t2sIJ<6|u_AWBW;uboq&2FoBHxczBhN2U!Uc#=*G-PKLuM`OV7}C(yl&7}qI_ z%HbvpmM|3|iuV^n->CCqFg?#vCk~d^=@Rlxj-aLGOT3uMLW5t$jfuJ3-h9bA2^QWf zTh38-$tpi5y}8@0&KN{e!vl~ebwUGe0c|#)XSJ4#iX^day~1Sodl6J z@Ef_{a#7gQ-jsZ!vUMee-38}kPF~D7-p5K+8t0?jLdk7oR5v_ysVU)C?m_pJi6!|! zdACGF1m(cZCczQs;nYU*=W@B-8M2ureIySeSE3wVl`Mz9k#7X2_F_F?BPF_s0bz7%czPyYg_K%E$&yRnSkRnn}nxp$q zTz)*?_|KS=KKw^>LkURH$nrtr#c@G5Qv2D`=!3ebYttaVJ`$*>X(S)8mCwP?EO&-T<@%Q2X{o8mulK0=nx932KJ5dJW`zo6>F*dmcBNqmzwWcY+zANw9cU&#; zR6c|gN9sf6yW@Dt&ungruMYTbB=c%2F}wqL>*w6q#PGc+zNx7{5-EK7+>Ev! zjf=)2T3E7amWKxB4If}hD8oZy5tqM{6z3M|-J#8n!v0$!iE<7y$u}G^`vOHTH4o%L z6EaZ=p;dVu>hH;T&D<(gUjiD(WKBJuu*h@Y3BmCBOR8`>Ei=xQu1T0x;S0<+g+do1 zAYuO=T>ZIB^>^1v>m6H0&E~=E3rRhgd@V2MK4HkUY-8%HB{T10>3gd)Qh@0rQ+&e) zv7O|!JPv%diLR*+teT~BS)RGA{p_J_}get}r~Bdz9W{CEAwBPB%p&Dp7m84)C51^fHgrv3`=AAPF0-0Dc#6RK~mOYJrN znQ2w8|M_1s2Zuo-*C18GHUgFWmSbnw{My(dj}jx)yLvG8n(Z;kuN(XJnOBUxm_Kyr z!q4QT*nI=$AG1pWP8yRr#eZ@vd;`uOX7q8qzv`bC@6TL)f4u*7Q@^tQgg>FJuk-$O znm>~JpPcRw$a9DMZiLU;!13l0o2R6rO1`b&I+m<%OPHqzL3$Kc6t5TDYS91{YTmzTX5gb59 zPfILECn-p&k~oYU{lr;PY6TtzF2V{4z*&V}WD6p)7+zu=foq==MazXpisFzH*XK85 zlY=ioI9xF*i{+KZ-8z=~Qi-+{5hk3FVKd`NrJX#V%-xPq!??{-y2<%k34)2QDvH*3 zK@F18S>{CLKJ<=oq>yRq;_+(9kf$hFFb*lMP~};r1;70yMx9GA@u-$R^H0S>*~^0&T=5y>bffv`2HNk~|!!(O0Z|90CuLvW4uG!qDqrD|X2$yEN1z zmVJ%@l<`g^aV2|8rtLh1>H;o$@z`fH3ZNcRTyqJc?p`aS@=AU!QRzsc|GMl*l_CYW zhkkRs{!fb1pctCoy#@SAb|g8JD>Y@(Hm3ZKg`zcKA*$h}{0~-8<$vz4fkE&cL2s>q z4tz(N0KHxo(1FECQKl5o`K-1JiIzCOVYB!jeX+%fiq3Yx zTny0@LUpB=w=H0VQmzOuQhEX|J%vGG_R<*{x#K8SE9+hk$*a-yKGn0%ruhX*07t^4 zP^le7>!Pn?lq5ovRk_^5Sf%hzUc#jLr4xsbW*-UguCz=DIQ%!+QEzUw;B+=*ct~Jb z)Gm{T18oyI0%~ znG_Ee=Z_pANvyLxbTRgz@=KdDwZ8AUCbIyY)31zv;fvNkC+gsW;fl;QWd^a1fg_146BP%DX*9Pkg9neF!sFBq4_CUGJR?vAW0L; z2pT1og`r_Mg*-$Tk_{`U+qqE)L?#SK+O=urP#vH7f=7do)E_3r|JnBd+1 z^Ry1KV%Wt9YF-Db1%kbrla58*sJ7428QMxV9Vy8%nS)ta#59wInyaPyG-i@=@M7{S zPrTxWc7I>BG0n8*W&M!!GWE-PtKd|GvfEEK;bmkIxcijSvP>hUUQBvVvL!r?dFpc# zgkW2`Hl10!v2N<;H8!l%F~bIIwBnt9 zoftK4Cwa-+Mduo8@(g@!9OuYMAzbQ5Ce<8~H)MEcT5Qs)F{G`^68m@er8oSXLd9wM z3_zJ=ugW{l4>BxMOe9EOA6=Wq!=qOxPq&Hmrfv=U@%r|)>Db%1jBK2p>zHX6QDGH) oe&d*@2iF1i$@#K29qZKzaMLvA>Fw(P$J^JYV{hM%c^FUrKi326 { + if ( currentYear !== fallbackCurrentYear ) { + setAttributes( { fallbackCurrentYear: currentYear } ); + } + }, [ currentYear, fallbackCurrentYear, setAttributes ] ); + + let displayDate; + + // Display the starting year as well if supplied by the user. + if ( showStartingYear && startingYear ) { + displayDate = startingYear + '–' + currentYear; + } else { + displayDate = currentYear; + } + return ( -
    - setAttributes( { message: val } ) } - /> -
    + <> + + + + setAttributes( { + showStartingYear: ! showStartingYear, + } ) + } + /> + { showStartingYear && ( + + setAttributes( { startingYear: value } ) + } + /> + ) } + + +

    © { displayDate }

    + ); } diff --git a/packages/create-block-tutorial-template/block-templates/editor.scss.mustache b/packages/create-block-tutorial-template/block-templates/editor.scss.mustache deleted file mode 100644 index 5d0b2deea6c10e..00000000000000 --- a/packages/create-block-tutorial-template/block-templates/editor.scss.mustache +++ /dev/null @@ -1,13 +0,0 @@ -/** - * The following styles get applied inside the editor only. - * - * Replace them with your own styles or remove the file completely. - */ - -.wp-block-{{namespace}}-{{slug}} input[type="text"] { - font-family: Gilbert, sans-serif; - font-size: 64px; - color: inherit; - background: inherit; - border: 0; -} diff --git a/packages/create-block-tutorial-template/block-templates/index.js.mustache b/packages/create-block-tutorial-template/block-templates/index.js.mustache index fa35fbf2729220..117d08b8669807 100644 --- a/packages/create-block-tutorial-template/block-templates/index.js.mustache +++ b/packages/create-block-tutorial-template/block-templates/index.js.mustache @@ -5,49 +5,41 @@ */ import { registerBlockType } from '@wordpress/blocks'; -/** - * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. - * All files containing `style` keyword are bundled together. The code used - * gets applied both to the front of your site and to the editor. All other files - * get applied to the editor only. - * - * @see https://www.npmjs.com/package/@wordpress/scripts#using-css - */ -import './style.scss'; -import './editor.scss'; - /** * Internal dependencies */ import Edit from './edit'; -{{#isStaticVariant}} import save from './save'; -{{/isStaticVariant}} import metadata from './block.json'; +/** + * Define a custom SVG icon for the block. This icon will appear in + * the Inserter and when the user selects the block in the Editor. + */ +const calendarIcon = ( + +); + /** * Every block starts by registering a new block type definition. * * @see https://developer.wordpress.org/block-editor/developers/block-api/#registering-a-block */ registerBlockType( metadata.name, { - /** - * Used to construct a preview for the block to be shown in the block inserter. - */ - example: { - attributes: { - message: '{{title}}', - }, - }, + icon: calendarIcon, /** * @see ./edit.js */ edit: Edit, - {{#isStaticVariant}} - /** * @see ./save.js */ save, - {{/isStaticVariant}} } ); diff --git a/packages/create-block-tutorial-template/block-templates/render.php.mustache b/packages/create-block-tutorial-template/block-templates/render.php.mustache index b971a023985e55..1d31cdbed2836b 100644 --- a/packages/create-block-tutorial-template/block-templates/render.php.mustache +++ b/packages/create-block-tutorial-template/block-templates/render.php.mustache @@ -1,10 +1,33 @@ -{{#isDynamicVariant}} -

    > - -

    -{{/isDynamicVariant}} + +// Get the current year. +$current_year = date( "Y" ); + +// Determine which content to display. +if ( isset( $attributes['fallbackCurrentYear'] ) && $attributes['fallbackCurrentYear'] === $current_year ) { + + // The current year is the same as the fallback, so use the block content saved in the database (by the save.js function). + $block_content = $content; +} else { + + // The current year is different from the fallback, so render the updated block content. + if ( ! empty( $attributes['startingYear'] ) && ! empty( $attributes['showStartingYear'] ) ) { + $display_date = $attributes['startingYear'] . '–' . $current_year; + } else { + $display_date = $current_year; + } + + $block_content = '

    © ' . esc_html( $display_date ) . '

    '; +} + +echo wp_kses_post( $block_content ); diff --git a/packages/create-block-tutorial-template/block-templates/save.js.mustache b/packages/create-block-tutorial-template/block-templates/save.js.mustache index a11ee432a4d442..944ae4c29254c4 100644 --- a/packages/create-block-tutorial-template/block-templates/save.js.mustache +++ b/packages/create-block-tutorial-template/block-templates/save.js.mustache @@ -1,4 +1,3 @@ -{{#isStaticVariant}} /** * React hook that is used to mark the block wrapper element. * It provides all the necessary props like the class name. @@ -16,10 +15,27 @@ import { useBlockProps } from '@wordpress/block-editor'; * * @param {Object} props Properties passed to the function. * @param {Object} props.attributes Available block attributes. + * * @return {Element} Element to render. */ export default function save( { attributes } ) { - const blockProps = useBlockProps.save(); - return
    { attributes.message }
    ; + const { fallbackCurrentYear, showStartingYear, startingYear } = attributes; + + // If there is no fallbackCurrentYear, which could happen if the block + // is loaded from a template/pattern, return null. In this case, block + // rendering will be handled by the render.php file. + if ( ! fallbackCurrentYear ) { + return null; + } + + let displayDate; + + // Display the starting year as well if supplied by the user. + if ( showStartingYear && startingYear ) { + displayDate = startingYear + '–' + fallbackCurrentYear; + } else { + displayDate = fallbackCurrentYear; + } + + return

    © { displayDate }

    ; } -{{/isStaticVariant}} diff --git a/packages/create-block-tutorial-template/block-templates/style.scss.mustache b/packages/create-block-tutorial-template/block-templates/style.scss.mustache deleted file mode 100644 index b1f1241345cbb9..00000000000000 --- a/packages/create-block-tutorial-template/block-templates/style.scss.mustache +++ /dev/null @@ -1,17 +0,0 @@ -/** - * The following styles get applied both on the front of your site - * and in the editor. - * - * Replace them with your own styles or remove the file completely. - */ - -@font-face { - font-family: Gilbert; - src: url(../assets/gilbert-color.otf); - font-weight: 700; -} - -.wp-block-{{namespace}}-{{slug}} { - font-family: Gilbert, sans-serif; - font-size: 64px; -} diff --git a/packages/create-block-tutorial-template/index.js b/packages/create-block-tutorial-template/index.js index 514918e70ad8b9..12c073d841fe55 100644 --- a/packages/create-block-tutorial-template/index.js +++ b/packages/create-block-tutorial-template/index.js @@ -5,35 +5,35 @@ const { join } = require( 'path' ); module.exports = { defaultValues: { - slug: 'gutenpride', - category: 'text', - title: 'Gutenpride', - description: - 'A Gutenberg block to show your pride! This block enables you to type text and style it with the color font Gilbert from Type with Pride.', - dashicon: 'flag', + slug: 'copyright-date-block', + title: 'Copyright Date', + description: "Display your site's copyright date.", attributes: { - message: { + fallbackCurrentYear: { + type: 'string', + }, + showStartingYear: { + type: 'boolean', + }, + startingYear: { type: 'string', - source: 'text', - selector: 'div', }, }, supports: { + color: { + background: false, + text: true, + }, html: false, - }, - }, - variants: { - static: {}, - dynamic: { - attributes: { - message: { - type: 'string', - }, + typography: { + fontSize: true, }, - render: 'file:./render.php', }, + editorScript: 'file:./index.js', + render: 'file:./render.php', + example: {}, + wpEnv: true, }, pluginTemplatesPath: join( __dirname, 'plugin-templates' ), blockTemplatesPath: join( __dirname, 'block-templates' ), - assetsPath: join( __dirname, 'assets' ), }; diff --git a/packages/create-block-tutorial-template/package.json b/packages/create-block-tutorial-template/package.json index 5fd4d71fd4cd8c..33eb73835bfaee 100644 --- a/packages/create-block-tutorial-template/package.json +++ b/packages/create-block-tutorial-template/package.json @@ -1,15 +1,16 @@ { "name": "@wordpress/create-block-tutorial-template", "version": "2.33.0", - "description": "Template for @wordpress/create-block used in the official WordPress tutorial.", + "description": "This is a template for @wordpress/create-block that creates an example 'Copyright Date' block. This block is used in the official WordPress block development Quick Start Guide.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ "wordpress", "create block", - "block template" + "block template", + "quick start" ], - "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/docs/getting-started/create-block", + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/docs/getting-started/quick-start-guide", "repository": { "type": "git", "url": "https://github.com/WordPress/gutenberg.git", diff --git a/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache b/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache index 52c9c4966646fa..6de24df94c8d83 100644 --- a/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache +++ b/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache @@ -8,7 +8,7 @@ * Description: {{description}} {{/description}} * Version: {{version}} - * Requires at least: 6.1 + * Requires at least: 6.2 * Requires PHP: 7.0 {{#author}} * Author: {{author}} @@ -41,7 +41,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @see https://developer.wordpress.org/reference/functions/register_block_type/ */ -function {{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init() { +function {{namespaceSnakeCase}}_{{slugSnakeCase}}_init() { register_block_type( __DIR__ . '/build' ); } -add_action( 'init', '{{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init' ); +add_action( 'init', '{{namespaceSnakeCase}}_{{slugSnakeCase}}_init' ); diff --git a/packages/create-block-tutorial-template/plugin-templates/readme.txt.mustache b/packages/create-block-tutorial-template/plugin-templates/readme.txt.mustache index f069906fb19fa5..d2b31bed6c0e60 100644 --- a/packages/create-block-tutorial-template/plugin-templates/readme.txt.mustache +++ b/packages/create-block-tutorial-template/plugin-templates/readme.txt.mustache @@ -3,7 +3,7 @@ Contributors: {{author}} {{/author}} Tags: block -Tested up to: 6.1 +Tested up to: 6.4 Stable tag: {{version}} {{#license}} License: {{license}} diff --git a/packages/create-block/README.md b/packages/create-block/README.md index 88ac97c34c2cf7..20bb6c62ccf66c 100644 --- a/packages/create-block/README.md +++ b/packages/create-block/README.md @@ -15,7 +15,7 @@ _It is largely inspired by [create-react-app](https://create-react-app.dev/docs/ - [Interactive Mode](#interactive-mode) - [`slug`](#slug) - [`options`](#options) -- [Available Commands](#available-commands) +- [Available Commands](#available-commands-in-the-scaffolded-project) - [External Project Templates](#external-project-templates) - [Contributing to this package](#contributing-to-this-package) @@ -141,7 +141,7 @@ For example, running the `start` script from inside the generated folder (`npm s ## External Project Templates -[Click here](https://github.com/WordPress/gutenberg/tree/HEAD/packages/create-block/docs/external-template.md) for information on External Project Templates +[Click here](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/packages-create-block-external-template/) for information on External Project Templates ## Contributing to this package diff --git a/packages/create-block/lib/templates.js b/packages/create-block/lib/templates.js index cf8274a21d3c03..5edaa3fcc8ba83 100644 --- a/packages/create-block/lib/templates.js +++ b/packages/create-block/lib/templates.js @@ -269,8 +269,7 @@ const getVariantVars = ( variants, variant ) => { for ( const variantName of variantNames ) { const key = variantName.charAt( 0 ).toUpperCase() + variantName.slice( 1 ); - variantVars[ `is${ key }Variant` ] = - currentVariant === variantName ?? false; + variantVars[ `is${ key }Variant` ] = currentVariant === variantName; } return variantVars; diff --git a/packages/customize-widgets/src/components/header/index.js b/packages/customize-widgets/src/components/header/index.js index 201831b90cd78a..34e4573c719dd5 100644 --- a/packages/customize-widgets/src/components/header/index.js +++ b/packages/customize-widgets/src/components/header/index.js @@ -6,11 +6,15 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { createPortal, useState, useEffect } from '@wordpress/element'; -import { __, _x, isRTL } from '@wordpress/i18n'; -import { ToolbarButton } from '@wordpress/components'; -import { NavigableToolbar } from '@wordpress/block-editor'; +import { Popover, ToolbarButton } from '@wordpress/components'; +import { useViewportMatch } from '@wordpress/compose'; +import { + NavigableToolbar, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { createPortal, useEffect, useRef, useState } from '@wordpress/element'; import { displayShortcut, isAppleOS } from '@wordpress/keycodes'; +import { __, _x, isRTL } from '@wordpress/i18n'; import { plus, undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; /** @@ -18,6 +22,9 @@ import { plus, undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; */ import Inserter from '../inserter'; import MoreMenu from '../more-menu'; +import { unlock } from '../../lock-unlock'; + +const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); function Header( { sidebar, @@ -26,6 +33,8 @@ function Header( { setIsInserterOpened, isFixedToolbarActive, } ) { + const isLargeViewport = useViewportMatch( 'medium' ); + const blockToolbarRef = useRef(); const [ [ hasUndo, hasRedo ], setUndoRedo ] = useState( [ sidebar.hasUndo(), sidebar.hasRedo(), @@ -98,6 +107,18 @@ function Header( { , inserter.contentContainer[ 0 ] ) } + + { isFixedToolbarActive && isLargeViewport && ( + <> +
    + +
    + + + ) } ); } diff --git a/packages/customize-widgets/src/style.scss b/packages/customize-widgets/src/style.scss index bd6d16b89c7fa7..3bf341c34c0eb1 100644 --- a/packages/customize-widgets/src/style.scss +++ b/packages/customize-widgets/src/style.scss @@ -17,34 +17,3 @@ .customize-widgets-popover { @include reset; } - -/** - Fixed bloock toolbar overrides. We can't detect each editor instance - in the styles of the block editor component so we need to override - the fixed styles here because the breakpoint css does not fire in the - customizer's left panel. -*/ -.block-editor-block-contextual-toolbar { - &.is-fixed { - position: sticky; - top: 0; - left: 0; - z-index: z-index(".block-editor-block-list__insertion-point"); - width: calc(100% + 2 * 12px); //12px is the padding of customizer sidebar content - - overflow-y: hidden; - - border: none; - border-bottom: $border-width solid $gray-200; - border-radius: 0; - - .block-editor-block-toolbar .components-toolbar-group, - .block-editor-block-toolbar .components-toolbar { - border-right-color: $gray-200; - } - - &.is-collapsed { - margin-left: -12px; //12px is the padding of customizer sidebar content - } - } -} diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 62da6a005c0d0b..84cf7ed617f185 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -193,7 +193,14 @@ function useStaticSelect( storeName ) { function useMappingSelect( suspense, mapSelect, deps ) { const registry = useRegistry(); const isAsync = useAsyncMode(); - const store = useMemo( () => Store( registry, suspense ), [ registry ] ); + const store = useMemo( + () => Store( registry, suspense ), + [ registry, suspense ] + ); + + // These are "pass-through" dependencies from the parent hook, + // and the parent should catch any hook rule violations. + // eslint-disable-next-line react-hooks/exhaustive-deps const selector = useCallback( mapSelect, deps ); const { subscribe, getValue } = store( selector, isAsync ); const result = useSyncExternalStore( subscribe, getValue, getValue ); diff --git a/packages/e2e-test-utils-playwright/package.json b/packages/e2e-test-utils-playwright/package.json index 775e1da771331e..f8d9b7cd2f0171 100644 --- a/packages/e2e-test-utils-playwright/package.json +++ b/packages/e2e-test-utils-playwright/package.json @@ -37,7 +37,8 @@ "form-data": "^4.0.0", "get-port": "^5.1.1", "lighthouse": "^10.4.0", - "mime": "^3.0.0" + "mime": "^3.0.0", + "web-vitals": "^3.5.0" }, "peerDependencies": { "@playwright/test": ">=1" diff --git a/packages/e2e-test-utils-playwright/src/admin/create-new-post.js b/packages/e2e-test-utils-playwright/src/admin/create-new-post.js deleted file mode 100644 index 81822e2514a731..00000000000000 --- a/packages/e2e-test-utils-playwright/src/admin/create-new-post.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * WordPress dependencies - */ -import { addQueryArgs } from '@wordpress/url'; - -/** - * Creates new post. - * - * @this {import('.').Editor} - * @param {Object} object Object to create new post, along with tips enabling option. - * @param {string} [object.postType] Post type of the new post. - * @param {string} [object.title] Title of the new post. - * @param {string} [object.content] Content of the new post. - * @param {string} [object.excerpt] Excerpt of the new post. - * @param {boolean} [object.showWelcomeGuide] Whether to show the welcome guide. - */ -export async function createNewPost( { - postType, - title, - content, - excerpt, - showWelcomeGuide = false, -} = {} ) { - const query = addQueryArgs( '', { - post_type: postType, - post_title: title, - content, - excerpt, - } ).slice( 1 ); - - await this.visitAdminPage( 'post-new.php', query ); - - await this.page.waitForFunction( ( welcomeGuide ) => { - if ( ! window?.wp?.data?.dispatch ) { - return false; - } - window.wp.data - .dispatch( 'core/preferences' ) - .set( 'core/edit-post', 'welcomeGuide', welcomeGuide ); - - window.wp.data - .dispatch( 'core/preferences' ) - .set( 'core/edit-post', 'fullscreenMode', false ); - - return true; - }, showWelcomeGuide ); -} diff --git a/packages/e2e-test-utils-playwright/src/admin/create-new-post.ts b/packages/e2e-test-utils-playwright/src/admin/create-new-post.ts new file mode 100644 index 00000000000000..e9cfefd9f65d5a --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/admin/create-new-post.ts @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import type { Admin } from './'; + +interface NewPostOptions { + postType?: string; + title?: string; + content?: string; + excerpt?: string; + showWelcomeGuide?: boolean; +} + +/** + * Creates new post. + * + * @param this + * @param options Options to create new post. + */ +export async function createNewPost( + this: Admin, + options: NewPostOptions = {} +) { + const query = new URLSearchParams(); + const { postType, title, content, excerpt } = options; + + if ( postType ) query.set( 'post_type', postType ); + if ( title ) query.set( 'post_title', title ); + if ( content ) query.set( 'content', content ); + if ( excerpt ) query.set( 'excerpt', excerpt ); + + await this.visitAdminPage( 'post-new.php', query.toString() ); + + await this.editor.setPreferences( 'core/edit-post', { + welcomeGuide: options.showWelcomeGuide ?? false, + fullscreenMode: false, + } ); +} diff --git a/packages/e2e-test-utils-playwright/src/admin/edit-post.ts b/packages/e2e-test-utils-playwright/src/admin/edit-post.ts new file mode 100644 index 00000000000000..77cf390d02aa01 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/admin/edit-post.ts @@ -0,0 +1,24 @@ +/** + * Internal dependencies + */ +import type { Admin } from '.'; + +/** + * Open the post with given ID in the editor. + * + * @param this + * @param postId Post ID to visit. + */ +export async function editPost( this: Admin, postId: string | number ) { + const query = new URLSearchParams(); + + query.set( 'post', String( postId ) ); + query.set( 'action', 'edit' ); + + await this.visitAdminPage( 'post.php', query.toString() ); + + await this.editor.setPreferences( 'core/edit-post', { + welcomeGuide: false, + fullscreenMode: false, + } ); +} diff --git a/packages/e2e-test-utils-playwright/src/admin/index.ts b/packages/e2e-test-utils-playwright/src/admin/index.ts index ed16564f8f4abb..08d2baf4b6520d 100644 --- a/packages/e2e-test-utils-playwright/src/admin/index.ts +++ b/packages/e2e-test-utils-playwright/src/admin/index.ts @@ -9,29 +9,36 @@ import type { Browser, Page, BrowserContext } from '@playwright/test'; import { createNewPost } from './create-new-post'; import { getPageError } from './get-page-error'; import { visitAdminPage } from './visit-admin-page'; +import { editPost } from './edit-post'; import { visitSiteEditor } from './visit-site-editor'; import type { PageUtils } from '../page-utils'; +import type { Editor } from '../editor'; type AdminConstructorProps = { page: Page; pageUtils: PageUtils; + editor: Editor; }; export class Admin { - browser: Browser; page: Page; - pageUtils: PageUtils; context: BrowserContext; + browser: Browser; + pageUtils: PageUtils; + editor: Editor; - constructor( { page, pageUtils }: AdminConstructorProps ) { + constructor( { page, pageUtils, editor }: AdminConstructorProps ) { this.page = page; this.context = page.context(); this.browser = this.context.browser()!; this.pageUtils = pageUtils; + this.editor = editor; } /** @borrows createNewPost as this.createNewPost */ createNewPost: typeof createNewPost = createNewPost.bind( this ); + /** @borrows editPost as this.editPost */ + editPost: typeof editPost = editPost.bind( this ); /** @borrows getPageError as this.getPageError */ getPageError: typeof getPageError = getPageError.bind( this ); /** @borrows visitAdminPage as this.visitAdminPage */ diff --git a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts index a545bdc704341c..5883a2b92a5bcc 100644 --- a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts +++ b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts @@ -1,74 +1,55 @@ -/** - * WordPress dependencies - */ -import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ import type { Admin } from './'; -export interface SiteEditorQueryParams { - postId: string | number; - postType: string; +interface SiteEditorOptions { + postId?: string | number; + postType?: string; + path?: string; + canvas?: string; + showWelcomeGuide?: boolean; } -const CANVAS_SELECTOR = 'iframe[title="Editor canvas"i] >> visible=true'; - /** - * Visits the Site Editor main page - * - * By default, it also skips the welcome guide. The option can be disabled if need be. + * Visits the Site Editor main page. * * @param this - * @param query Query params to be serialized as query portion of URL. - * @param skipWelcomeGuide Whether to skip the welcome guide as part of the navigation. + * @param options Options to visit the site editor. */ export async function visitSiteEditor( this: Admin, - query: SiteEditorQueryParams, - skipWelcomeGuide = true + options: SiteEditorOptions = {} ) { - const path = addQueryArgs( '', { - ...query, - } ).slice( 1 ); - - await this.visitAdminPage( 'site-editor.php', path ); - - if ( skipWelcomeGuide ) { - await this.page.evaluate( () => { - window.wp.data - .dispatch( 'core/preferences' ) - .set( 'core/edit-site', 'welcomeGuide', false ); - - window.wp.data - .dispatch( 'core/preferences' ) - .set( 'core/edit-site', 'welcomeGuideStyles', false ); - - window.wp.data - .dispatch( 'core/preferences' ) - .set( 'core/edit-site', 'welcomeGuidePage', false ); - - window.wp.data - .dispatch( 'core/preferences' ) - .set( 'core/edit-site', 'welcomeGuideTemplate', false ); + const { postId, postType, path, canvas } = options; + const query = new URLSearchParams(); + + if ( postId ) query.set( 'postId', String( postId ) ); + if ( postType ) query.set( 'postType', postType ); + if ( path ) query.set( 'path', path ); + if ( canvas ) query.set( 'canvas', canvas ); + + await this.visitAdminPage( 'site-editor.php', query.toString() ); + + if ( ! options.showWelcomeGuide ) { + await this.editor.setPreferences( 'core/edit-site', { + welcomeGuide: false, + welcomeGuideStyles: false, + welcomeGuidePage: false, + welcomeGuideTemplate: false, } ); } - // Check if the current page has an editor canvas first. - if ( ( await this.page.locator( CANVAS_SELECTOR ).count() ) > 0 ) { - // The site editor initially loads with an empty body, - // we need to wait for the editor canvas to be rendered. - await this.page - .frameLocator( CANVAS_SELECTOR ) - .locator( 'body > *' ) - .first() - .waitFor(); - } - - // TODO: Ideally the content underneath the canvas loader should be marked inert until it's ready. + /** + * @todo This is a workaround for the fact that the editor canvas is seen as + * ready and visible before the loading spinner is hidden. Ideally, the + * content underneath the loading overlay should be marked inert until the + * loading is done. + */ await this.page - .locator( '.edit-site-canvas-loader' ) + // Spinner was used instead of the progress bar in an earlier version of + // the site editor. + .locator( '.edit-site-canvas-loader, .edit-site-canvas-spinner' ) // Bigger timeout is needed for larger entities, for example the large // post html fixture that we load for performance tests, which often // doesn't make it under the default 10 seconds. diff --git a/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts b/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts index 2c473352c6123d..e8c02fb11dcb71 100644 --- a/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts +++ b/packages/e2e-test-utils-playwright/src/editor/click-block-options-menu-item.ts @@ -12,8 +12,7 @@ import type { Editor } from './index'; export async function clickBlockOptionsMenuItem( this: Editor, label: string ) { await this.clickBlockToolbarButton( 'Options' ); await this.page - .locator( - `role=menu[name="Options"i] >> role=menuitem[name="${ label }"i]` - ) + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: label } ) .click(); } diff --git a/packages/e2e-test-utils-playwright/src/editor/index.ts b/packages/e2e-test-utils-playwright/src/editor/index.ts index 8c10ba370b1a92..c222f68aecc90a 100644 --- a/packages/e2e-test-utils-playwright/src/editor/index.ts +++ b/packages/e2e-test-utils-playwright/src/editor/index.ts @@ -22,6 +22,7 @@ import { publishPost } from './publish-post'; import { saveDraft } from './save-draft'; import { selectBlocks } from './select-blocks'; import { setContent } from './set-content'; +import { setPreferences } from './set-preferences'; import { showBlockToolbar } from './show-block-toolbar'; import { saveSiteEditorEntities } from './site-editor'; import { setIsFixedToolbar } from './set-is-fixed-toolbar'; @@ -76,6 +77,8 @@ export class Editor { selectBlocks: typeof selectBlocks = selectBlocks.bind( this ); /** @borrows setContent as this.setContent */ setContent: typeof setContent = setContent.bind( this ); + /** @borrows setPreferences as this.setPreferences */ + setPreferences: typeof setPreferences = setPreferences.bind( this ); /** @borrows showBlockToolbar as this.showBlockToolbar */ showBlockToolbar: typeof showBlockToolbar = showBlockToolbar.bind( this ); /** @borrows setIsFixedToolbar as this.setIsFixedToolbar */ diff --git a/packages/e2e-test-utils-playwright/src/editor/set-preferences.ts b/packages/e2e-test-utils-playwright/src/editor/set-preferences.ts new file mode 100644 index 00000000000000..9e5345db4b9e39 --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/editor/set-preferences.ts @@ -0,0 +1,37 @@ +/** + * Internal dependencies + */ +import type { Editor } from './index'; + +type PreferencesContext = + | 'core/edit-post' + | 'core/edit-site' + | 'core/customize-widgets'; + +/** + * Set the preferences of the editor. + * + * @param this + * @param context Context to set preferences for. + * @param preferences Preferences to set. + */ +export async function setPreferences( + this: Editor, + context: PreferencesContext, + preferences: Record< string, any > +) { + await this.page.waitForFunction( () => window?.wp?.data ); + + await this.page.evaluate( + async ( props ) => { + for ( const [ key, value ] of Object.entries( + props.preferences + ) ) { + await window.wp.data + .dispatch( 'core/preferences' ) + .set( props.context, key, value ); + } + }, + { context, preferences } + ); +} diff --git a/packages/e2e-test-utils-playwright/src/metrics/index.ts b/packages/e2e-test-utils-playwright/src/metrics/index.ts index 68343f6d7c4829..fac05d9004bd8d 100644 --- a/packages/e2e-test-utils-playwright/src/metrics/index.ts +++ b/packages/e2e-test-utils-playwright/src/metrics/index.ts @@ -2,6 +2,11 @@ * External dependencies */ import type { Page, Browser } from '@playwright/test'; +import { join } from 'path'; +// resolution-mode support in TypeScript 5.3 will resolve this. +// See https://devblogs.microsoft.com/typescript/announcing-typescript-5-3-beta/ +// @ts-expect-error +import type { Metric } from 'web-vitals'; type EventType = | 'click' @@ -32,11 +37,22 @@ type MetricsConstructorProps = { page: Page; }; +interface WebVitalsMeasurements { + CLS?: number; + FCP?: number; + FID?: number; + INP?: number; + LCP?: number; + TTFB?: number; +} + export class Metrics { browser: Browser; page: Page; trace: Trace; + webVitals: WebVitalsMeasurements = {}; + constructor( { page }: MetricsConstructorProps ) { this.page = page; this.browser = page.context().browser()!; @@ -273,4 +289,95 @@ export class Metrics { ) .map( ( item ) => ( item.dur ? item.dur / 1000 : 0 ) ); } + + /** + * Initializes the web-vitals library upon next page navigation. + * + * Defaults to automatically triggering the navigation, + * but it can also be done manually. + * + * @example + * ```js + * await metrics.initWebVitals(); + * console.log( await metrics.getWebVitals() ); + * ``` + * + * @example + * ```js + * await metrics.initWebVitals( false ); + * await page.goto( '/some-other-page' ); + * console.log( await metrics.getWebVitals() ); + * ``` + * + * @param reload Whether to force navigation by reloading the current page. + */ + async initWebVitals( reload = true ) { + await this.page.addInitScript( { + path: join( + __dirname, + '../../../../node_modules/web-vitals/dist/web-vitals.umd.cjs' + ), + } ); + + await this.page.exposeFunction( + '__reportVitals__', + ( data: string ) => { + const measurement: Metric = JSON.parse( data ); + this.webVitals[ measurement.name ] = measurement.value; + } + ); + + await this.page.addInitScript( () => { + const reportVitals = ( measurement: unknown ) => + window.__reportVitals__( JSON.stringify( measurement ) ); + + window.addEventListener( 'DOMContentLoaded', () => { + // @ts-ignore + window.webVitals.onCLS( reportVitals ); + // @ts-ignore + window.webVitals.onFCP( reportVitals ); + // @ts-ignore + window.webVitals.onFID( reportVitals ); + // @ts-ignore + window.webVitals.onINP( reportVitals ); + // @ts-ignore + window.webVitals.onLCP( reportVitals ); + // @ts-ignore + window.webVitals.onTTFB( reportVitals ); + } ); + } ); + + if ( reload ) { + // By reloading the page the script will be applied. + await this.page.reload(); + } + } + + /** + * Returns web vitals as collected by the web-vitals library. + * + * If the web-vitals library hasn't been loaded on the current page yet, + * it will be initialized with a page reload. + * + * Reloads the page to force web-vitals to report all collected metrics. + * + * @return {WebVitalsMeasurements} Web vitals measurements. + */ + async getWebVitals() { + // Reset values. + this.webVitals = {}; + + const hasScript = await this.page.evaluate( + () => typeof window.webVitals !== 'undefined' + ); + + if ( ! hasScript ) { + await this.initWebVitals(); + } + + // Trigger navigation so the web-vitals library reports values on unload. + await this.page.reload(); + + return this.webVitals; + } } diff --git a/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts b/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts index fa43fc76d27c33..d0af0b499f286b 100644 --- a/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts +++ b/packages/e2e-test-utils-playwright/src/page-utils/drag-files.ts @@ -9,7 +9,7 @@ import { getType } from 'mime'; * Internal dependencies */ import type { PageUtils } from './index'; -import type { ElementHandle, Locator } from '@playwright/test'; +import type { Locator } from '@playwright/test'; type FileObject = { name: string; @@ -118,25 +118,29 @@ async function dragFiles( /** * Drop the files at the current position. - * - * @param locator */ - drop: async ( locator: Locator | ElementHandle | null ) => { - if ( ! locator ) { - const topMostElement = await this.page.evaluateHandle( - ( { x, y } ) => { - return document.elementFromPoint( x, y ); - }, - position - ); - locator = topMostElement.asElement(); - } + drop: async () => { + const topMostElement = await this.page.evaluateHandle( + ( { x, y } ) => { + const element = document.elementFromPoint( x, y ); + if ( element instanceof HTMLIFrameElement ) { + const offsetBox = element.getBoundingClientRect(); + return element.contentDocument!.elementFromPoint( + x - offsetBox.x, + y - offsetBox.y + ); + } + return element; + }, + position + ); + const elementHandle = topMostElement.asElement(); - if ( ! locator ) { + if ( ! elementHandle ) { throw new Error( 'Element not found.' ); } - const dataTransfer = await locator.evaluateHandle( + const dataTransfer = await elementHandle.evaluateHandle( async ( _node, _fileObjects ) => { const dt = new DataTransfer(); const fileInstances = await Promise.all( @@ -159,7 +163,7 @@ async function dragFiles( fileObjects ); - await locator.dispatchEvent( 'drop', { dataTransfer } ); + await elementHandle.dispatchEvent( 'drop', { dataTransfer } ); await cdpSession.detach(); }, diff --git a/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts index 778f71b6d770e1..677eff31955bd1 100644 --- a/packages/e2e-test-utils-playwright/src/test.ts +++ b/packages/e2e-test-utils-playwright/src/test.ts @@ -119,8 +119,8 @@ const test = base.extend< lighthousePort: number; } >( { - admin: async ( { page, pageUtils }, use ) => { - await use( new Admin( { page, pageUtils } ) ); + admin: async ( { page, pageUtils, editor }, use ) => { + await use( new Admin( { page, pageUtils, editor } ) ); }, editor: async ( { page }, use ) => { await use( new Editor( { page } ) ); diff --git a/packages/e2e-test-utils-playwright/src/types.ts b/packages/e2e-test-utils-playwright/src/types.ts index 62e38033b73e8f..3dd6b0683aa33c 100644 --- a/packages/e2e-test-utils-playwright/src/types.ts +++ b/packages/e2e-test-utils-playwright/src/types.ts @@ -1,29 +1,12 @@ +/** + * External dependencies + */ declare global { interface Window { // Silence the warning for `window.wp` in Playwright's evaluate functions. wp: any; - } - - // Experimental API that is subject to change. - // See https://developer.mozilla.org/en-US/docs/Web/API/LayoutShiftAttribution - interface LayoutShiftAttribution { - readonly node: Node; - readonly previousRect: DOMRectReadOnly; - readonly currentRect: DOMRectReadOnly; - readonly toJSON: () => string; - } - - // Experimental API that is subject to change. - // See https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift - interface LayoutShift extends PerformanceEntry { - readonly duration: number; - readonly entryType: 'layout-shift'; - readonly name: 'layout-shift'; - readonly startTime: DOMHighResTimeStamp; - readonly value: number; - readonly hadRecentInput: boolean; - readonly lastInputTime: DOMHighResTimeStamp; - readonly sources: LayoutShiftAttribution[]; + // Helper function added by Metrics fixture for web-vitals.js. + __reportVitals__: ( data: string ) => void; } } diff --git a/packages/e2e-tests/plugins/interactive-blocks.php b/packages/e2e-tests/plugins/interactive-blocks.php index a6bd468493840d..c551127548e801 100644 --- a/packages/e2e-tests/plugins/interactive-blocks.php +++ b/packages/e2e-tests/plugins/interactive-blocks.php @@ -39,8 +39,8 @@ function () { // HTML is not correct or malformed. if ( 'true' === $_GET['disable_directives_ssr'] ) { remove_filter( - 'render_block', - 'gutenberg_interactivity_process_directives_in_root_blocks' + 'render_block_data', + 'gutenberg_interactivity_mark_root_blocks' ); } } diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/container-blocks.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/container-blocks.test.js.snap deleted file mode 100644 index cbcbf0402f8c96..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/container-blocks.test.js.snap +++ /dev/null @@ -1,58 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Container block without paragraph support ensures we can use the alternative block appender properly 1`] = ` -" - -
    - -" -`; - -exports[`InnerBlocks Template Sync Ensure inner block writing flow works as expected without additional paragraphs added 1`] = ` -" - -

    Test Paragraph

    - -" -`; - -exports[`InnerBlocks Template Sync Ensures blocks without locking are kept intact even if they do not match the template 1`] = ` -" - -

    Content…

    - - - -

    added paragraph

    - - -" -`; - -exports[`InnerBlocks Template Sync Removes blocks that are not expected by the template if a lock all exists 1`] = ` -" - -

    Content…

    - -" -`; - -exports[`InnerBlocks Template Sync Synchronizes blocks if lock 'all' is set and the template prop is changed 1`] = ` -" - -

    Content…

    - -" -`; - -exports[`InnerBlocks Template Sync Synchronizes blocks if lock 'all' is set and the template prop is changed 2`] = ` -" - -

    Content…

    - - - -

    Two

    - -" -`; diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/cpt-locking.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/cpt-locking.test.js.snap deleted file mode 100644 index 5c6867e75ad7af..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/cpt-locking.test.js.snap +++ /dev/null @@ -1,147 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`cpt locking template_lock all should insert line breaks when using enter and shift-enter 1`] = ` -" -
    - - - -

    First line
    Second line
    Third line

    - - - -
    -

    -
    - - - -
    -" -`; - -exports[`cpt locking template_lock all should not error when deleting the cotents of a paragraph 1`] = ` -" -
    - - - -

    - - - -
    -

    -
    - - - -
    -" -`; - -exports[`cpt locking template_lock all unlocked group should allow blocks to be moved 1`] = ` -" -
    -

    p1

    - - - -
    -

    -
    -
    -" -`; - -exports[`cpt locking template_lock all unlocked group should allow blocks to be removed 1`] = ` -" -
    -
    -

    -
    -
    -" -`; - -exports[`cpt locking template_lock false should allow blocks to be inserted 1`] = ` -" -
    - - - -

    - - - -
    -

    -
    - - - -
    - - - -
      -
    • List content
    • -
    -" -`; - -exports[`cpt locking template_lock false should allow blocks to be moved 1`] = ` -" -

    p1

    - - - -
    - - - -
    -

    -
    - - - -
    -" -`; - -exports[`cpt locking template_lock false should allow blocks to be removed 1`] = ` -" -
    - - - -
    -

    -
    - - - -
    -" -`; - -exports[`cpt locking template_lock insert should allow blocks to be moved 1`] = ` -" -

    p1

    - - - -
    - - - -
    -

    -
    - - - -
    -" -`; diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/inner-blocks-render-appender.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/inner-blocks-render-appender.test.js.snap deleted file mode 100644 index 232e6e02b398eb..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/inner-blocks-render-appender.test.js.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RenderAppender prop of InnerBlocks Users can customize the appender and can still insert blocks using exposed components 1`] = ` -" -
    -
    -

    -
    -
    -" -`; - -exports[`RenderAppender prop of InnerBlocks Users can dynamically customize the appender 1`] = ` -" -
    -
    -

    -
    - - - -
    -
    -" -`; diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/meta-attribute-block.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/meta-attribute-block.test.js.snap deleted file mode 100644 index 268c8b45d059f2..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/meta-attribute-block.test.js.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Block with a meta attribute Early Registration Should persist the meta attribute properly 1`] = `""`; - -exports[`Block with a meta attribute Early Registration Should persist the meta attribute properly in a different post type 1`] = `""`; - -exports[`Block with a meta attribute Late Registration Should persist the meta attribute properly 1`] = `""`; - -exports[`Block with a meta attribute Late Registration Should persist the meta attribute properly in a different post type 1`] = `""`; diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/plugins-api.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/plugins-api.test.js.snap deleted file mode 100644 index 9e4a5ac3ad6f61..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/plugins-api.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Using Plugins API Document Setting Custom Panel Should render a custom panel inside Document Setting sidebar 1`] = `"My Custom Panel"`; - -exports[`Using Plugins API Sidebar Medium screen Should open plugins sidebar using More Menu item and render content 1`] = `"
    (no title)
    Plugin title
    "`; - -exports[`Using Plugins API Sidebar Should open plugins sidebar using More Menu item and render content 1`] = `"
    (no title)
    Plugin title
    "`; diff --git a/packages/e2e-tests/specs/editor/plugins/annotations.test.js b/packages/e2e-tests/specs/editor/plugins/annotations.test.js deleted file mode 100644 index 50290aa1cc68bb..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/annotations.test.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - clickBlockToolbarButton, - clickMenuItem, - clickOnMoreMenuItem, - createNewPost, - deactivatePlugin, - canvas, -} from '@wordpress/e2e-test-utils'; - -const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => { - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( buttonLabel ); -}; - -const ANNOTATIONS_SELECTOR = '.annotation-text-e2e-tests'; - -describe( 'Annotations', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-plugin-plugins-api' ); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-plugin-plugins-api' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - /** - * Annotates the text in the first block from start to end. - * - * @param {number} start Position to start the annotation. - * @param {number} end Position to end the annotation. - * - * @return {void} - */ - async function annotateFirstBlock( start, end ) { - await page.focus( '#annotations-tests-range-start' ); - await page.keyboard.press( 'Backspace' ); - await page.keyboard.type( start + '' ); - await page.focus( '#annotations-tests-range-end' ); - await page.keyboard.press( 'Backspace' ); - await page.keyboard.type( end + '' ); - - // Click add annotation button. - const addAnnotationButton = ( - await page.$x( "//button[contains(text(), 'Add annotation')]" ) - )[ 0 ]; - await addAnnotationButton.click(); - await canvas().evaluate( () => - document.querySelector( '.wp-block-paragraph' ).focus() - ); - } - - /** - * Presses the button that removes all annotations. - * - * @return {void} - */ - async function removeAnnotations() { - // Click remove annotations button. - const addAnnotationButton = ( - await page.$x( "//button[contains(text(), 'Remove annotations')]" ) - )[ 0 ]; - await addAnnotationButton.click(); - await canvas().evaluate( () => - document.querySelector( '[contenteditable]' ).focus() - ); - } - - /** - * Returns the inner text of the first text annotation on the page. - * - * @return {Promise} The annotated text. - */ - async function getAnnotatedText() { - const annotations = await canvas().$$( ANNOTATIONS_SELECTOR ); - - const annotation = annotations[ 0 ]; - - return await canvas().evaluate( ( el ) => el.innerText, annotation ); - } - - /** - * Returns the inner HTML of the first RichText in the page. - * - * @return {Promise} Inner HTML. - */ - async function getRichTextInnerHTML() { - const htmlContent = await canvas().$$( '.wp-block-paragraph' ); - return await canvas().evaluate( ( el ) => { - return el.innerHTML; - }, htmlContent[ 0 ] ); - } - - it( 'allows a block to be annotated', async () => { - await page.keyboard.type( 'Title' + '\n' + 'Paragraph to annotate' ); - - await clickOnMoreMenuItem( 'Annotations' ); - - let annotations = await canvas().$$( ANNOTATIONS_SELECTOR ); - expect( annotations ).toHaveLength( 0 ); - - await annotateFirstBlock( 9, 13 ); - - annotations = await canvas().$$( ANNOTATIONS_SELECTOR ); - expect( annotations ).toHaveLength( 1 ); - - const text = await getAnnotatedText(); - expect( text ).toBe( ' to ' ); - - await clickOnBlockSettingsMenuItem( 'Edit as HTML' ); - - const htmlContent = await canvas().$$( - '.block-editor-block-list__block-html-textarea' - ); - const html = await canvas().evaluate( ( el ) => { - return el.innerHTML; - }, htmlContent[ 0 ] ); - - // There should be no tags in the raw content. - expect( html ).toBe( '<p>Paragraph to annotate</p>' ); - } ); - - it( 'keeps the cursor in the same location when applying annotation', async () => { - await page.keyboard.type( 'Title' + '\n' + 'ABC' ); - await clickOnMoreMenuItem( 'Annotations' ); - - await annotateFirstBlock( 1, 2 ); - - // The selection should still be at the end, so test that by typing: - await page.keyboard.type( 'D' ); - - await removeAnnotations(); - const htmlContent = await canvas().$$( '.wp-block-paragraph' ); - const html = await canvas().evaluate( ( el ) => { - return el.innerHTML; - }, htmlContent[ 0 ] ); - - expect( html ).toBe( 'ABCD' ); - } ); - - it( 'moves when typing before it', async () => { - await page.keyboard.type( 'Title' + '\n' + 'ABC' ); - await clickOnMoreMenuItem( 'Annotations' ); - - await annotateFirstBlock( 1, 2 ); - - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - - // Put an 1 after the A, it should not be annotated. - await page.keyboard.type( '1' ); - - const annotatedText = await getAnnotatedText(); - expect( annotatedText ).toBe( 'B' ); - - await removeAnnotations(); - const blockText = await getRichTextInnerHTML(); - expect( blockText ).toBe( 'A1BC' ); - } ); - - it( 'grows when typing inside it', async () => { - await page.keyboard.type( 'Title' + '\n' + 'ABC' ); - await clickOnMoreMenuItem( 'Annotations' ); - - await annotateFirstBlock( 1, 2 ); - - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - - // Put an 1 after the A, it should not be annotated. - await page.keyboard.type( '2' ); - - const annotatedText = await getAnnotatedText(); - expect( annotatedText ).toBe( 'B2' ); - - await removeAnnotations(); - const blockText = await getRichTextInnerHTML(); - expect( blockText ).toBe( 'AB2C' ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js deleted file mode 100644 index c7ca368003397e..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/child-blocks.test.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - closeGlobalBlockInserter, - createNewPost, - deactivatePlugin, - getAllBlockInserterItemTitles, - insertBlock, - openGlobalBlockInserter, -} from '@wordpress/e2e-test-utils'; - -describe( 'Child Blocks', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-child-blocks' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-child-blocks' ); - } ); - - it( 'are hidden from the global block inserter', async () => { - await openGlobalBlockInserter(); - await expect( await getAllBlockInserterItemTitles() ).not.toContain( - 'Child Blocks Child' - ); - } ); - - it( 'shows up in a parent block', async () => { - await insertBlock( 'Child Blocks Unrestricted Parent' ); - await closeGlobalBlockInserter(); - await page.waitForSelector( - '[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender' - ); - await page.click( - '[data-type="test/child-blocks-unrestricted-parent"] .block-editor-default-block-appender' - ); - await openGlobalBlockInserter(); - const inserterItemTitles = await getAllBlockInserterItemTitles(); - expect( inserterItemTitles ).toContain( 'Child Blocks Child' ); - expect( inserterItemTitles.length ).toBeGreaterThan( 20 ); - } ); - - it( 'display in a parent block with allowedItems', async () => { - await insertBlock( 'Child Blocks Restricted Parent' ); - await closeGlobalBlockInserter(); - await page.waitForSelector( - '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' - ); - await page.click( - '[data-type="test/child-blocks-restricted-parent"] .block-editor-default-block-appender' - ); - await openGlobalBlockInserter(); - const allowedBlocks = await getAllBlockInserterItemTitles(); - expect( allowedBlocks.sort() ).toEqual( [ - 'Child Blocks Child', - 'Image', - 'Paragraph', - ] ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/container-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/container-blocks.test.js deleted file mode 100644 index 7393b2700ff2b7..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/container-blocks.test.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - getEditedPostContent, - insertBlock, - switchEditorModeTo, - pressKeyWithModifier, - canvas, -} from '@wordpress/e2e-test-utils'; - -describe( 'InnerBlocks Template Sync', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-innerblocks-templates' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-innerblocks-templates' ); - } ); - - const insertBlockAndAddParagraphInside = async ( blockName, blockSlug ) => { - const paragraphToAdd = ` - -

    added paragraph

    - - `; - await insertBlock( blockName ); - await switchEditorModeTo( 'Code' ); - await page.$eval( - '.editor-post-text-editor', - ( element, _paragraph, _blockSlug ) => { - const blockDelimiter = ``; - element.value = element.value.replace( - blockDelimiter, - `${ _paragraph }${ blockDelimiter }` - ); - }, - paragraphToAdd, - blockSlug - ); - // Press "Enter" inside the code editor to fire the `onChange` event for the new value. - await page.click( '.editor-post-text-editor' ); - await pressKeyWithModifier( 'primary', 'A' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'Enter' ); - await switchEditorModeTo( 'Visual' ); - }; - - it( 'Ensures blocks without locking are kept intact even if they do not match the template', async () => { - await insertBlockAndAddParagraphInside( - 'Test Inner Blocks no locking', - 'test/test-inner-blocks-no-locking' - ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'Removes blocks that are not expected by the template if a lock all exists', async () => { - await insertBlockAndAddParagraphInside( - 'Test InnerBlocks locking all', - 'test/test-inner-blocks-locking-all' - ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - // Test for regressions of https://github.com/WordPress/gutenberg/issues/27897. - it( `Synchronizes blocks if lock 'all' is set and the template prop is changed`, async () => { - // Insert the template and assert that the template has its initial value. - await insertBlock( 'Test Inner Blocks update locked template' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Trigger a template update and assert that a second block is now present. - const [ button ] = await canvas().$x( - `//button[contains(text(), 'Update template')]` - ); - await button.click(); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'Ensure inner block writing flow works as expected without additional paragraphs added', async () => { - const TEST_BLOCK_NAME = 'Test Inner Blocks Paragraph Placeholder'; - - await insertBlock( TEST_BLOCK_NAME ); - await page.keyboard.type( 'Test Paragraph' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); - -describe( 'Container block without paragraph support', () => { - beforeAll( async () => { - await activatePlugin( - 'gutenberg-test-container-block-without-paragraph' - ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( - 'gutenberg-test-container-block-without-paragraph' - ); - } ); - - it( 'ensures we can use the alternative block appender properly', async () => { - await insertBlock( 'Container without paragraph' ); - - // Open the specific appender used when there's no paragraph support. - await page.click( - '.block-editor-inner-blocks .block-list-appender .block-list-appender__toggle' - ); - - // Insert an image block. - const insertButton = ( - await page.$x( `//button//span[contains(text(), 'Image')]` ) - )[ 0 ]; - await insertButton.click(); - - // Check the inserted content. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js b/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js deleted file mode 100644 index 447be0793fafbb..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/cpt-locking.test.js +++ /dev/null @@ -1,251 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - clickBlockToolbarButton, - clickMenuItem, - createNewPost, - deactivatePlugin, - getEditedPostContent, - insertBlock, - pressKeyTimes, - pressKeyWithModifier, - setPostContent, - canvas, -} from '@wordpress/e2e-test-utils'; - -describe( 'cpt locking', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-plugin-cpt-locking' ); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-plugin-cpt-locking' ); - } ); - - const shouldDisableTheInserter = async () => { - expect( - await page.evaluate( () => { - const inserter = document.querySelector( - '.edit-post-header [aria-label="Add block"], .edit-post-header [aria-label="Toggle block inserter"]' - ); - return inserter.getAttribute( 'disabled' ); - } ) - ).not.toBeNull(); - }; - - const shouldNotAllowBlocksToBeRemoved = async () => { - await canvas().type( - '.block-editor-rich-text__editable[data-type="core/paragraph"]', - 'p1' - ); - await clickBlockToolbarButton( 'Options' ); - expect( - await page.$x( '//button/span[contains(text(), "Delete")]' ) - ).toHaveLength( 0 ); - }; - - const shouldAllowBlocksToBeMoved = async () => { - await canvas().click( - 'div > .block-editor-rich-text__editable[data-type="core/paragraph"]' - ); - expect( await page.$( 'button[aria-label="Move up"]' ) ).not.toBeNull(); - await page.click( 'button[aria-label="Move up"]' ); - await canvas().type( - 'div > .block-editor-rich-text__editable[data-type="core/paragraph"]', - 'p1' - ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - }; - - describe( 'template_lock all', () => { - beforeEach( async () => { - await createNewPost( { postType: 'locked-all-post' } ); - } ); - - it( 'should disable the inserter', shouldDisableTheInserter ); - - it( - 'should not allow blocks to be removed', - shouldNotAllowBlocksToBeRemoved - ); - - it( 'should not allow blocks to be moved', async () => { - await canvas().click( - '.block-editor-rich-text__editable[data-type="core/paragraph"]' - ); - expect( await page.$( 'button[aria-label="Move up"]' ) ).toBeNull(); - } ); - - it( 'should not error when deleting the cotents of a paragraph', async () => { - await canvas().click( - '.block-editor-block-list__block[data-type="core/paragraph"]' - ); - const textToType = 'Paragraph'; - await page.keyboard.type( 'Paragraph' ); - await pressKeyTimes( 'Backspace', textToType.length + 1 ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should insert line breaks when using enter and shift-enter', async () => { - await canvas().click( - '.block-editor-block-list__block[data-type="core/paragraph"]' - ); - await page.keyboard.type( 'First line' ); - await pressKeyTimes( 'Enter', 1 ); - await page.keyboard.type( 'Second line' ); - await pressKeyWithModifier( 'shift', 'Enter' ); - await page.keyboard.type( 'Third line' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should show invalid template notice if the blocks do not match the templte', async () => { - const content = await getEditedPostContent(); - const [ , contentWithoutImage ] = - content.split( '' ); - await setPostContent( contentWithoutImage ); - const noticeContent = await page.waitForSelector( - '.editor-template-validation-notice .components-notice__content' - ); - expect( - await page.evaluate( - ( _noticeContent ) => _noticeContent.firstChild.nodeValue, - noticeContent - ) - ).toEqual( - 'The content of your post doesn’t match the template assigned to your post type.' - ); - } ); - - it( 'should not allow blocks to be inserted in inner blocks', async () => { - await canvas().click( - 'button[aria-label="Two columns; equal split"]' - ); - await page.evaluate( - () => new Promise( window.requestIdleCallback ) - ); - expect( - await canvas().$( - '.wp-block-column .block-editor-button-block-appender' - ) - ).toBeNull(); - - expect( - await page.evaluate( () => { - const inserter = document.querySelector( - '.edit-post-header [aria-label="Add block"], .edit-post-header [aria-label="Toggle block inserter"]' - ); - return inserter.getAttribute( 'disabled' ); - } ) - ).not.toBeNull(); - } ); - } ); - - describe( 'template_lock insert', () => { - beforeEach( async () => { - await createNewPost( { postType: 'locked-insert-post' } ); - } ); - - it( 'should disable the inserter', shouldDisableTheInserter ); - - it( - 'should not allow blocks to be removed', - shouldNotAllowBlocksToBeRemoved - ); - - it( 'should allow blocks to be moved', shouldAllowBlocksToBeMoved ); - } ); - - describe( 'template_lock false', () => { - beforeEach( async () => { - await createNewPost( { postType: 'not-locked-post' } ); - } ); - - it( 'should allow blocks to be inserted', async () => { - expect( - // "Add block" selector is required to make sure performance comparison - // doesn't fail on older branches where we still had "Add block" as label. - await page.$( - '.edit-post-header [aria-label="Add block"], .edit-post-header [aria-label="Toggle block inserter"]' - ) - ).not.toBeNull(); - await insertBlock( 'List' ); - await page.keyboard.type( 'List content' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should allow blocks to be removed', async () => { - await canvas().type( - '.block-editor-rich-text__editable[data-type="core/paragraph"]', - 'p1' - ); - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Delete' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should allow blocks to be moved', shouldAllowBlocksToBeMoved ); - } ); - - describe( 'template_lock all unlocked group', () => { - beforeEach( async () => { - await createNewPost( { - postType: 'l-post-ul-group', - } ); - } ); - - it( 'should allow blocks to be removed', async () => { - await canvas().type( - 'div > .block-editor-rich-text__editable[data-type="core/paragraph"]', - 'p1' - ); - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Delete' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should allow blocks to be moved', shouldAllowBlocksToBeMoved ); - } ); - - describe( 'template_lock all locked group', () => { - beforeEach( async () => { - await createNewPost( { - postType: 'l-post-l-group', - } ); - } ); - - it( - 'should not allow blocks to be removed', - shouldNotAllowBlocksToBeRemoved - ); - - it( 'should not allow blocks to be moved', async () => { - await canvas().click( - '.block-editor-rich-text__editable[data-type="core/paragraph"]' - ); - expect( await page.$( 'button[aria-label="Move up"]' ) ).toBeNull(); - } ); - } ); - - describe( 'template_lock all inherited group', () => { - beforeEach( async () => { - await createNewPost( { - postType: 'l-post-i-group', - } ); - } ); - - it( - 'should not allow blocks to be removed', - shouldNotAllowBlocksToBeRemoved - ); - - it( 'should not allow blocks to be moved', async () => { - await canvas().click( - '.block-editor-rich-text__editable[data-type="core/paragraph"]' - ); - expect( await page.$( 'button[aria-label="Move up"]' ) ).toBeNull(); - } ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js b/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js deleted file mode 100644 index 7621fbea12140a..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/inner-blocks-prioritized-inserter-blocks.test.js +++ /dev/null @@ -1,132 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - getAllBlockInserterItemTitles, - insertBlock, - closeGlobalBlockInserter, - canvas, -} from '@wordpress/e2e-test-utils'; - -const QUICK_INSERTER_RESULTS_SELECTOR = - '.block-editor-inserter__quick-inserter-results'; - -describe( 'Prioritized Inserter Blocks Setting on InnerBlocks', () => { - beforeAll( async () => { - await activatePlugin( - 'gutenberg-test-innerblocks-prioritized-inserter-blocks' - ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( - 'gutenberg-test-innerblocks-prioritized-inserter-blocks' - ); - } ); - - describe( 'Quick inserter', () => { - it( 'uses defaulting ordering if prioritzed blocks setting was not set', async () => { - const parentBlockSelector = - '[data-type="test/prioritized-inserter-blocks-unset"]'; - await insertBlock( 'Prioritized Inserter Blocks Unset' ); - await closeGlobalBlockInserter(); - - await page.waitForSelector( parentBlockSelector ); - - await page.click( - `${ parentBlockSelector } .block-list-appender .block-editor-inserter__toggle` - ); - - await page.waitForSelector( QUICK_INSERTER_RESULTS_SELECTOR ); - - await expect( await getAllBlockInserterItemTitles() ).toHaveLength( - 6 - ); - } ); - - it( 'uses the priority ordering if prioritzed blocks setting is set', async () => { - const parentBlockSelector = - '[data-type="test/prioritized-inserter-blocks-set"]'; - await insertBlock( 'Prioritized Inserter Blocks Set' ); - await closeGlobalBlockInserter(); - - await page.waitForSelector( parentBlockSelector ); - - await page.click( - `${ parentBlockSelector } .block-list-appender .block-editor-inserter__toggle` - ); - - await page.waitForSelector( QUICK_INSERTER_RESULTS_SELECTOR ); - - // Should still be only 6 results regardless of the priority ordering. - const inserterItems = await getAllBlockInserterItemTitles(); - - // Should still be only 6 results regardless of the priority ordering. - expect( inserterItems ).toHaveLength( 6 ); - - expect( inserterItems.slice( 0, 3 ) ).toEqual( [ - 'Audio', - 'Spacer', - 'Code', - ] ); - } ); - - it( 'obeys allowed blocks over prioritzed blocks setting if conflicted', async () => { - const parentBlockSelector = - '[data-type="test/prioritized-inserter-blocks-set-with-conflicting-allowed-blocks"]'; - await insertBlock( - 'Prioritized Inserter Blocks Set With Conflicting Allowed Blocks' - ); - await closeGlobalBlockInserter(); - - await page.waitForSelector( parentBlockSelector ); - - await page.click( - `${ parentBlockSelector } .block-list-appender .block-editor-inserter__toggle` - ); - - await page.waitForSelector( QUICK_INSERTER_RESULTS_SELECTOR ); - - const inserterItems = await getAllBlockInserterItemTitles(); - - expect( inserterItems.slice( 0, 3 ) ).toEqual( [ - 'Spacer', - 'Code', - 'Paragraph', - ] ); - expect( inserterItems ).toEqual( - expect.not.arrayContaining( [ 'Audio' ] ) - ); - } ); - } ); - describe( 'Slash inserter', () => { - it( 'uses the priority ordering if prioritzed blocks setting is set', async () => { - await insertBlock( 'Prioritized Inserter Blocks Set' ); - await canvas().click( '[data-type="core/image"]' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '/' ); - // Wait for the results to display. - await page.waitForSelector( '.components-autocomplete__result' ); - const inserterItemTitles = await page.evaluate( () => { - return Array.from( - document.querySelectorAll( - '.components-autocomplete__result' - ) - ).map( ( { innerText } ) => innerText ); - } ); - expect( inserterItemTitles ).toHaveLength( 9 ); // Default suggested blocks number. - expect( inserterItemTitles.slice( 0, 3 ) ).toEqual( [ - 'Audio', - 'Spacer', - 'Code', - ] ); - } ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/inner-blocks-render-appender.test.js b/packages/e2e-tests/specs/editor/plugins/inner-blocks-render-appender.test.js deleted file mode 100644 index 1322713b033e24..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/inner-blocks-render-appender.test.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - getAllBlockInserterItemTitles, - getEditedPostContent, - insertBlock, - closeGlobalBlockInserter, -} from '@wordpress/e2e-test-utils'; - -const INSERTER_RESULTS_SELECTOR = - '.block-editor-inserter__quick-inserter-results'; -const QUOTE_INSERT_BUTTON_SELECTOR = '//button[.="Quote"]'; -const APPENDER_SELECTOR = '.my-custom-awesome-appender'; -const DYNAMIC_APPENDER_SELECTOR = 'my-dynamic-blocks-appender'; - -describe( 'RenderAppender prop of InnerBlocks', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-innerblocks-render-appender' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-innerblocks-render-appender' ); - } ); - - it( 'Users can customize the appender and can still insert blocks using exposed components', async () => { - // Insert the InnerBlocks renderAppender block. - await insertBlock( 'InnerBlocks renderAppender' ); - await closeGlobalBlockInserter(); - // Wait for the custom block appender to appear. - await page.waitForSelector( APPENDER_SELECTOR ); - // Verify if the custom block appender text is the expected one. - expect( - await page.evaluate( - ( el ) => el.innerText, - await page.$( `${ APPENDER_SELECTOR } > span` ) - ) - ).toEqual( 'My custom awesome appender' ); - - // Open the inserter of our custom block appender and expand all the categories. - await page.click( - `${ APPENDER_SELECTOR } .block-editor-button-block-appender` - ); - // Verify if the blocks the custom inserter is rendering are the expected ones. - expect( await getAllBlockInserterItemTitles() ).toEqual( [ - 'Quote', - 'Video', - ] ); - - // Find the quote block insert button option within the inserter popover. - const inserterPopover = await page.$( INSERTER_RESULTS_SELECTOR ); - const quoteButton = ( - await inserterPopover.$x( QUOTE_INSERT_BUTTON_SELECTOR ) - )[ 0 ]; - - // Insert a quote block. - await quoteButton.click(); - // Verify if the post content is the expected one e.g: the quote was inserted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'Users can dynamically customize the appender', async () => { - // Insert the InnerBlocks renderAppender dynamic block. - await insertBlock( 'InnerBlocks renderAppender dynamic' ); - await closeGlobalBlockInserter(); - - // Wait for the custom dynamic block appender to appear. - await page.waitForSelector( '.' + DYNAMIC_APPENDER_SELECTOR ); - - // Verify if the custom block appender text is the expected one. - await page.waitForXPath( - `//*[contains(@class, "${ DYNAMIC_APPENDER_SELECTOR }")]/span[contains(@class, "empty-blocks-appender")][contains(text(), "Empty Blocks Appender")]` - ); - - // Open the inserter of our custom block appender and expand all the categories. - const blockAppenderButtonSelector = `.${ DYNAMIC_APPENDER_SELECTOR } .block-editor-button-block-appender`; - await page.click( blockAppenderButtonSelector ); - - // Verify if the blocks the custom inserter is rendering are the expected ones. - expect( await getAllBlockInserterItemTitles() ).toEqual( [ - 'Quote', - 'Video', - ] ); - - // Find the quote block insert button option within the inserter popover. - const inserterPopover = await page.$( INSERTER_RESULTS_SELECTOR ); - const quoteButton = ( - await inserterPopover.$x( QUOTE_INSERT_BUTTON_SELECTOR ) - )[ 0 ]; - - // Insert a quote block. - await quoteButton.click(); - - // Select the quote block. - await page.keyboard.press( 'ArrowDown' ); - - // Verify if the custom block appender text changed as expected. - await page.waitForXPath( - `//*[contains(@class, "${ DYNAMIC_APPENDER_SELECTOR }")]/span[contains(@class, "single-blocks-appender")][contains(text(), "Single Blocks Appender")]` - ); - - // Verify that the custom appender button is still being rendered. - expect( await page.$( blockAppenderButtonSelector ) ).toBeTruthy(); - - // Insert a video block. - await insertBlock( 'Video' ); - - // Verify if the custom block appender text changed as expected. - await page.waitForXPath( - `//*[contains(@class, "${ DYNAMIC_APPENDER_SELECTOR }")]/span[contains(@class, "multiple-blocks-appender")][contains(text(), "Multiple Blocks Appender")]` - ); - - // Verify that the custom appender button is now not being rendered. - expect( await page.$( blockAppenderButtonSelector ) ).toBeFalsy(); - - // Verify that final block markup is the expected one. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/meta-attribute-block.test.js b/packages/e2e-tests/specs/editor/plugins/meta-attribute-block.test.js deleted file mode 100644 index a95d11b4f7dadb..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/meta-attribute-block.test.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - getEditedPostContent, - insertBlock, - saveDraft, - pressKeyTimes, -} from '@wordpress/e2e-test-utils'; - -describe( 'Block with a meta attribute', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-meta-attribute-block' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-meta-attribute-block' ); - } ); - - describe.each( [ [ 'Early Registration' ], [ 'Late Registration' ] ] )( - '%s', - ( variant ) => { - it( 'Should persist the meta attribute properly', async () => { - await insertBlock( `Test Meta Attribute Block (${ variant })` ); - await page.keyboard.type( 'Value' ); - - // Regression Test: Previously the caret would wrongly reset to the end - // of any input for meta-sourced attributes, due to syncing behavior of - // meta attribute updates. - // - // See: https://github.com/WordPress/gutenberg/issues/15739 - await pressKeyTimes( 'ArrowLeft', 5 ); - await page.keyboard.type( 'Meta ' ); - - await saveDraft(); - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - const persistedValue = await page.evaluate( - () => document.querySelector( '.my-meta-input' ).value - ); - expect( persistedValue ).toBe( 'Meta Value' ); - } ); - - it( 'Should use the same value in all the blocks', async () => { - await insertBlock( `Test Meta Attribute Block (${ variant })` ); - await insertBlock( `Test Meta Attribute Block (${ variant })` ); - await insertBlock( `Test Meta Attribute Block (${ variant })` ); - await page.keyboard.type( 'Meta Value' ); - - const inputs = await page.$$( '.my-meta-input' ); - await Promise.all( - inputs.map( async ( input ) => { - // Clicking the input selects the block, - // and selecting the block enables the sync data mode - // as otherwise the asynchronous re-rendering of unselected blocks - // may cause the input to have not yet been updated for the other blocks. - await input.click(); - const inputValue = await input.getProperty( 'value' ); - expect( await inputValue.jsonValue() ).toBe( - 'Meta Value' - ); - } ) - ); - } ); - - it( 'Should persist the meta attribute properly in a different post type', async () => { - await createNewPost( { postType: 'page' } ); - await insertBlock( `Test Meta Attribute Block (${ variant })` ); - await page.keyboard.type( 'Value' ); - - // Regression Test: Previously the caret would wrongly reset to the end - // of any input for meta-sourced attributes, due to syncing behavior of - // meta attribute updates. - // - // See: https://github.com/WordPress/gutenberg/issues/15739 - await pressKeyTimes( 'ArrowLeft', 5 ); - await page.keyboard.type( 'Meta ' ); - - await saveDraft(); - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - const persistedValue = await page.evaluate( - () => document.querySelector( '.my-meta-input' ).value - ); - expect( persistedValue ).toBe( 'Meta Value' ); - } ); - } - ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js b/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js deleted file mode 100644 index 3a75f9656c71e5..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/meta-boxes.test.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - createNewPost, - deactivatePlugin, - findSidebarPanelToggleButtonWithTitle, - insertBlock, - openDocumentSettingsSidebar, - publishPost, - saveDraft, -} from '@wordpress/e2e-test-utils'; - -describe( 'Meta boxes', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-plugin-meta-box' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-plugin-meta-box' ); - } ); - - it( 'Should save the post', async () => { - // Save should not be an option for new empty post. - expect( await page.$( '.editor-post-save-draft' ) ).toBe( null ); - - // Add title to enable valid non-empty post save. - await page.type( '.editor-post-title__input', 'Hello Meta' ); - expect( await page.$( '.editor-post-save-draft' ) ).not.toBe( null ); - - await saveDraft(); - - // After saving, affirm that the button returns to Save Draft. - await page.waitForSelector( '.editor-post-save-draft' ); - } ); - - it( 'Should render dynamic blocks when the meta box uses the excerpt for front end rendering', async () => { - // Publish a post so there's something for the latest posts dynamic block to render. - await page.type( '.editor-post-title__input', 'A published post' ); - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Hello there!' ); - await publishPost(); - - // Publish a post with the latest posts dynamic block. - await createNewPost(); - await page.type( '.editor-post-title__input', 'Dynamic block test' ); - await insertBlock( 'Latest Posts' ); - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Check the dynamic block appears. - const latestPostsBlock = await page.waitForSelector( - '.wp-block-latest-posts' - ); - - expect( - await latestPostsBlock.evaluate( ( block ) => block.textContent ) - ).toContain( 'A published post' ); - - expect( - await latestPostsBlock.evaluate( ( block ) => block.textContent ) - ).toContain( 'Dynamic block test' ); - } ); - - it( 'Should render the excerpt in meta based on post content if no explicit excerpt exists', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Excerpt from content.' ); - await page.type( '.editor-post-title__input', 'A published post' ); - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Retrieve the excerpt used as meta. - const metaExcerpt = await page.evaluate( () => { - return document - .querySelector( 'meta[property="gutenberg:hello"]' ) - .getAttribute( 'content' ); - } ); - - expect( metaExcerpt ).toEqual( 'Excerpt from content.' ); - } ); - - it( 'Should render the explicitly set excerpt in meta instead of the content based one', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Excerpt from content.' ); - await page.type( '.editor-post-title__input', 'A published post' ); - - // Open the excerpt panel. - await openDocumentSettingsSidebar(); - const excerptButton = - await findSidebarPanelToggleButtonWithTitle( 'Excerpt' ); - if ( excerptButton ) { - await excerptButton.click( 'button' ); - } - - await page.waitForSelector( '.editor-post-excerpt textarea' ); - - await page.type( - '.editor-post-excerpt textarea', - 'Explicitly set excerpt.' - ); - - await publishPost(); - - // View the post. - const viewPostLinks = await page.$x( - "//a[contains(text(), 'View Post')]" - ); - await viewPostLinks[ 0 ].click(); - await page.waitForNavigation(); - - // Retrieve the excerpt used as meta. - const metaExcerpt = await page.evaluate( () => { - return document - .querySelector( 'meta[property="gutenberg:hello"]' ) - .getAttribute( 'content' ); - } ); - - expect( metaExcerpt ).toEqual( 'Explicitly set excerpt.' ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/plugins/plugins-api.test.js b/packages/e2e-tests/specs/editor/plugins/plugins-api.test.js deleted file mode 100644 index 4ad8d0e634204f..00000000000000 --- a/packages/e2e-tests/specs/editor/plugins/plugins-api.test.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * WordPress dependencies - */ -import { - activatePlugin, - clickBlockAppender, - clickOnMoreMenuItem, - createNewPost, - deactivatePlugin, - openDocumentSettingsSidebar, - openPublishPanel, - publishPost, - setBrowserViewport, -} from '@wordpress/e2e-test-utils'; - -describe( 'Using Plugins API', () => { - beforeAll( async () => { - await activatePlugin( 'gutenberg-test-plugin-plugins-api' ); - } ); - - afterAll( async () => { - await deactivatePlugin( 'gutenberg-test-plugin-plugins-api' ); - } ); - - beforeEach( async () => { - await createNewPost(); - } ); - - describe( 'Post Status Info', () => { - it( 'Should render post status info inside Document Setting sidebar', async () => { - await openDocumentSettingsSidebar(); - - const pluginPostStatusInfoText = await page.$eval( - '.edit-post-post-status .my-post-status-info-plugin', - ( el ) => el.innerText - ); - expect( pluginPostStatusInfoText ).toBe( 'My post status info' ); - } ); - } ); - - describe( 'Publish Panel', () => { - beforeEach( async () => { - // Type something first to activate Publish button. - await clickBlockAppender(); - await page.keyboard.type( 'First paragraph' ); - } ); - - it( 'Should render publish panel inside Pre-publish sidebar', async () => { - await openPublishPanel(); - - const pluginPublishPanelText = await page.$eval( - '.editor-post-publish-panel .my-publish-panel-plugin__pre', - ( el ) => el.innerText - ); - expect( pluginPublishPanelText ).toMatch( 'My pre publish panel' ); - } ); - - it( 'Should render publish panel inside Post-publish sidebar', async () => { - await publishPost(); - const pluginPublishPanel = await page.waitForSelector( - '.editor-post-publish-panel .my-publish-panel-plugin__post' - ); - const pluginPublishPanelText = await pluginPublishPanel.evaluate( - ( node ) => node.innerText - ); - expect( pluginPublishPanelText ).toMatch( 'My post publish panel' ); - } ); - } ); - - describe( 'Sidebar', () => { - const SIDEBAR_PINNED_ITEM_BUTTON = - '.interface-pinned-items button[aria-label="Plugin title"]'; - const SIDEBAR_PANEL_SELECTOR = '.sidebar-title-plugin-panel'; - it( 'Should open plugins sidebar using More Menu item and render content', async () => { - await clickOnMoreMenuItem( 'Plugin more menu title' ); - - const pluginSidebarContent = await page.$eval( - '.edit-post-sidebar', - ( el ) => el.innerHTML - ); - expect( pluginSidebarContent ).toMatchSnapshot(); - } ); - - it( 'Should be pinned by default and can be opened and closed using pinned items', async () => { - const sidebarPinnedItem = await page.$( - SIDEBAR_PINNED_ITEM_BUTTON - ); - expect( sidebarPinnedItem ).not.toBeNull(); - await sidebarPinnedItem.click(); - expect( await page.$( SIDEBAR_PANEL_SELECTOR ) ).not.toBeNull(); - await sidebarPinnedItem.click(); - expect( await page.$( SIDEBAR_PANEL_SELECTOR ) ).toBeNull(); - } ); - - it( 'Can be pinned and unpinned', async () => { - await ( await page.$( SIDEBAR_PINNED_ITEM_BUTTON ) ).click(); - const unpinButton = await page.$( - 'button[aria-label="Unpin from toolbar"]' - ); - await unpinButton.click(); - expect( await page.$( SIDEBAR_PINNED_ITEM_BUTTON ) ).toBeNull(); - await page.click( - '.interface-complementary-area-header button[aria-label="Close plugin"]' - ); - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - expect( await page.$( SIDEBAR_PINNED_ITEM_BUTTON ) ).toBeNull(); - await clickOnMoreMenuItem( 'Plugin more menu title' ); - await page.click( 'button[aria-label="Pin to toolbar"]' ); - expect( await page.$( SIDEBAR_PINNED_ITEM_BUTTON ) ).not.toBeNull(); - await page.reload(); - await page.waitForSelector( '.edit-post-layout' ); - expect( await page.$( SIDEBAR_PINNED_ITEM_BUTTON ) ).not.toBeNull(); - } ); - - it( 'Should close plugins sidebar using More Menu item', async () => { - await clickOnMoreMenuItem( 'Plugin more menu title' ); - - const pluginSidebarOpened = await page.$( '.edit-post-sidebar' ); - expect( pluginSidebarOpened ).not.toBeNull(); - - await clickOnMoreMenuItem( 'Plugin more menu title' ); - - const pluginSidebarClosed = await page.$( '.edit-post-sidebar' ); - expect( pluginSidebarClosed ).toBeNull(); - } ); - - describe( 'Medium screen', () => { - beforeAll( async () => { - await setBrowserViewport( 'medium' ); - } ); - - afterAll( async () => { - await setBrowserViewport( 'large' ); - } ); - - it( 'Should open plugins sidebar using More Menu item and render content', async () => { - await clickOnMoreMenuItem( 'Plugin more menu title' ); - - const pluginSidebarContent = await page.$eval( - '.edit-post-sidebar', - ( el ) => el.innerHTML - ); - expect( pluginSidebarContent ).toMatchSnapshot(); - } ); - } ); - } ); - - describe( 'Document Setting Custom Panel', () => { - it( 'Should render a custom panel inside Document Setting sidebar', async () => { - await openDocumentSettingsSidebar(); - const pluginDocumentSettingsText = await page.$eval( - '.edit-post-sidebar .my-document-setting-plugin', - ( el ) => el.innerText - ); - expect( pluginDocumentSettingsText ).toMatchSnapshot(); - } ); - } ); - - describe( 'Error Boundary', () => { - beforeAll( async () => { - await activatePlugin( - 'gutenberg-test-plugin-plugins-error-boundary' - ); - } ); - - afterAll( async () => { - await deactivatePlugin( - 'gutenberg-test-plugin-plugins-error-boundary' - ); - } ); - - it( 'Should create notice using plugin error boundary callback', async () => { - const noticeContent = await page.waitForSelector( - '.is-error .components-notice__content' - ); - expect( - await page.evaluate( - ( _noticeContent ) => _noticeContent.firstChild.nodeValue, - noticeContent - ) - ).toEqual( - 'The "my-error-plugin" plugin has encountered an error and cannot be rendered.' - ); - - expect( console ).toHaveErrored(); - } ); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/publish-button.test.js b/packages/e2e-tests/specs/editor/various/publish-button.test.js deleted file mode 100644 index 90ef0950e535bb..00000000000000 --- a/packages/e2e-tests/specs/editor/various/publish-button.test.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * WordPress dependencies - */ -import { - arePrePublishChecksEnabled, - disablePrePublishChecks, - enablePrePublishChecks, - createNewPost, - canvas, -} from '@wordpress/e2e-test-utils'; - -describe( 'PostPublishButton', () => { - let werePrePublishChecksEnabled; - beforeEach( async () => { - await createNewPost(); - werePrePublishChecksEnabled = await arePrePublishChecksEnabled(); - if ( werePrePublishChecksEnabled ) { - await disablePrePublishChecks(); - } - } ); - - afterEach( async () => { - if ( werePrePublishChecksEnabled ) { - await enablePrePublishChecks(); - } - } ); - - it( 'should be disabled when post is not saveable', async () => { - const publishButton = await page.$( - '.editor-post-publish-button[aria-disabled="true"]' - ); - expect( publishButton ).not.toBeNull(); - } ); - - it( 'should be disabled when post is being saved', async () => { - await canvas().type( '.editor-post-title__input', 'E2E Test Post' ); // Make it saveable. - expect( - await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) - ).toBeNull(); - - await page.click( '.editor-post-save-draft' ); - expect( - await page.$( '.editor-post-publish-button[aria-disabled="true"]' ) - ).not.toBeNull(); - } ); -} ); diff --git a/packages/edit-post/src/components/header/document-actions/index.js b/packages/edit-post/src/components/header/document-actions/index.js index 52df978e2cd5b3..105b31e3122ac9 100644 --- a/packages/edit-post/src/components/header/document-actions/index.js +++ b/packages/edit-post/src/components/header/document-actions/index.js @@ -20,21 +20,18 @@ import { displayShortcut } from '@wordpress/keycodes'; import { store as editPostStore } from '../../../store'; function DocumentActions() { - const { template, isEditing } = useSelect( ( select ) => { - const { isEditingTemplate, getEditedPostTemplate } = - select( editPostStore ); - const _isEditing = isEditingTemplate(); + const { template } = useSelect( ( select ) => { + const { getEditedPostTemplate } = select( editPostStore ); return { - template: _isEditing ? getEditedPostTemplate() : null, - isEditing: _isEditing, + template: getEditedPostTemplate(), }; }, [] ); const { clearSelectedBlock } = useDispatch( blockEditorStore ); const { setIsEditingTemplate } = useDispatch( editPostStore ); const { open: openCommandCenter } = useDispatch( commandsStore ); - if ( ! isEditing || ! template ) { + if ( ! template ) { return null; } diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index b86e66af7a849e..9c650f8660da18 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -19,7 +19,6 @@ import { Button, ToolbarItem } from '@wordpress/components'; import { listView, plus } from '@wordpress/icons'; import { useRef, useCallback } from '@wordpress/element'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; -import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -33,7 +32,7 @@ const preventDefault = ( event ) => { event.preventDefault(); }; -function HeaderToolbar( { setListViewToggleElement } ) { +function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) { const inserterButton = useRef(); const { setIsInserterOpened, setIsListViewOpened } = useDispatch( editPostStore ); @@ -44,7 +43,6 @@ function HeaderToolbar( { setListViewToggleElement } ) { showIconLabels, isListViewOpen, listViewShortcut, - hasFixedToolbar, } = useSelect( ( select ) => { const { hasInserterItems, getBlockRootClientId, getBlockSelectionEnd } = select( blockEditorStore ); @@ -52,7 +50,6 @@ function HeaderToolbar( { setListViewToggleElement } ) { const { getEditorMode, isFeatureActive, isListViewOpened } = select( editPostStore ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); - const { get: getPreference } = select( preferencesStore ); return { // This setting (richEditingEnabled) should not live in the block editor's setting. @@ -69,7 +66,6 @@ function HeaderToolbar( { setListViewToggleElement } ) { listViewShortcut: getShortcutRepresentation( 'core/edit-post/toggle-list-view' ), - hasFixedToolbar: getPreference( 'core/edit-post', 'fixedToolbar' ), }; }, [] ); diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 2e0d470818fecd..c1c8222394979d 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -1,11 +1,28 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ +import { + privateApis as blockEditorPrivateApis, + store as blockEditorStore, +} from '@wordpress/block-editor'; import { PostSavedState, PostPreviewButton } from '@wordpress/editor'; +import { useEffect, useRef, useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { next, previous } from '@wordpress/icons'; import { PinnedItems } from '@wordpress/interface'; import { useViewportMatch } from '@wordpress/compose'; -import { __unstableMotion as motion } from '@wordpress/components'; +import { + Button, + __unstableMotion as motion, + Popover, +} from '@wordpress/components'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -19,6 +36,9 @@ import ViewLink from '../view-link'; import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; import DocumentActions from './document-actions'; +import { unlock } from '../../lock-unlock'; + +const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); const slideY = { hidden: { y: '-50px' }, @@ -36,18 +56,43 @@ function Header( { setEntitiesSavedStatesCallback, setListViewToggleElement, } ) { - const isLargeViewport = useViewportMatch( 'large' ); - const { hasActiveMetaboxes, isPublishSidebarOpened, showIconLabels } = - useSelect( - ( select ) => ( { - hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), - isPublishSidebarOpened: - select( editPostStore ).isPublishSidebarOpened(), - showIconLabels: - select( editPostStore ).isFeatureActive( 'showIconLabels' ), - } ), - [] - ); + const isWideViewport = useViewportMatch( 'large' ); + const isLargeViewport = useViewportMatch( 'medium' ); + const blockToolbarRef = useRef(); + const { + blockSelectionStart, + hasActiveMetaboxes, + hasFixedToolbar, + isEditingTemplate, + isPublishSidebarOpened, + showIconLabels, + } = useSelect( ( select ) => { + const { get: getPreference } = select( preferencesStore ); + + return { + blockSelectionStart: + select( blockEditorStore ).getBlockSelectionStart(), + hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), + isEditingTemplate: select( editPostStore ).isEditingTemplate(), + isPublishSidebarOpened: + select( editPostStore ).isPublishSidebarOpened(), + hasFixedToolbar: getPreference( 'core/edit-post', 'fixedToolbar' ), + showIconLabels: + select( editPostStore ).isFeatureActive( 'showIconLabels' ), + }; + }, [] ); + + const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = + useState( true ); + + const hasBlockSelected = !! blockSelectionStart; + + useEffect( () => { + // If we have a new block selection, show the block tools + if ( blockSelectionStart ) { + setIsBlockToolsCollapsed( false ); + } + }, [ blockSelectionStart ] ); return (
    @@ -65,10 +110,52 @@ function Header( { className="edit-post-header__toolbar" > -
    - + { hasFixedToolbar && isLargeViewport && ( + <> +
    + +
    + + { isEditingTemplate && hasBlockSelected && ( + + } + > + { filters.map( ( filter ) => { + if ( filter.isVisible ) { + return null; + } + + return ( + } + > + { filter.name } + + } + > + { filter.elements.map( ( element ) => ( + { + onChangeView( ( currentView ) => ( { + ...currentView, + page: 1, + filters: [ + ...currentView.filters, + { + field: filter.field, + operator: 'in', + value: element.value, + }, + ], + } ) ); + } } + role="menuitemcheckbox" + > + { element.label } + + ) ) } + + ); + } ) } + + ); +} diff --git a/packages/edit-site/src/components/dataviews/dataviews.js b/packages/edit-site/src/components/dataviews/dataviews.js index 81a1224b4c9fec..e2e392f8fd95b6 100644 --- a/packages/edit-site/src/components/dataviews/dataviews.js +++ b/packages/edit-site/src/components/dataviews/dataviews.js @@ -16,6 +16,22 @@ import ViewActions from './view-actions'; import Filters from './filters'; import Search from './search'; import { ViewGrid } from './view-grid'; +import { ViewSideBySide } from './view-side-by-side'; + +// To do: convert to view type registry. +export const viewTypeSupportsMap = { + list: {}, + grid: {}, + 'side-by-side': { + preview: true, + }, +}; + +const viewTypeMap = { + list: ViewList, + grid: ViewGrid, + 'side-by-side': ViewSideBySide, +}; export default function DataViews( { view, @@ -27,8 +43,9 @@ export default function DataViews( { data, isLoading = false, paginationInfo, + supportedLayouts, } ) { - const ViewComponent = view.type === 'list' ? ViewList : ViewGrid; + const ViewComponent = viewTypeMap[ view.type ]; const _fields = useMemo( () => { return fields.map( ( field ) => ( { ...field, @@ -53,11 +70,12 @@ export default function DataViews( { onChangeView={ onChangeView } /> - + diff --git a/packages/edit-site/src/components/dataviews/filters.js b/packages/edit-site/src/components/dataviews/filters.js index 1cb2b36d6cda79..9c37a7501a155d 100644 --- a/packages/edit-site/src/components/dataviews/filters.js +++ b/packages/edit-site/src/components/dataviews/filters.js @@ -6,70 +6,77 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import InFilter from './in-filter'; +import { default as InFilter, OPERATOR_IN } from './in-filter'; +import AddFilter from './add-filter'; +import ResetFilters from './reset-filters'; + +const VALID_OPERATORS = [ OPERATOR_IN ]; export default function Filters( { fields, view, onChangeView } ) { - const filterIndex = {}; + const filters = []; fields.forEach( ( field ) => { if ( ! field.filters ) { return; } field.filters.forEach( ( filter ) => { - let id = field.id; - if ( 'string' === typeof filter ) { - filterIndex[ id ] = { - id, + if ( VALID_OPERATORS.some( ( operator ) => operator === filter ) ) { + filters.push( { + field: field.id, name: field.header, - type: filter, - }; - } - - if ( 'object' === typeof filter ) { - id = filter.id || field.id; - filterIndex[ id ] = { - id, - name: filter.name || field.header, - type: filter.type, - }; - } - - if ( 'enumeration' === filterIndex[ id ]?.type ) { - const elements = [ - { - value: filter.resetValue || '', - label: filter.resetLabel || __( 'All' ), - }, - ...( field.elements || [] ), - ]; - filterIndex[ id ] = { - ...filterIndex[ id ], - elements, - }; + operator: filter, + elements: [ + { + value: '', + label: __( 'All' ), + }, + ...( field.elements || [] ), + ], + isVisible: view.filters.some( + ( f ) => f.field === field.id && f.operator === filter + ), + } ); } } ); } ); - return ( - view.visibleFilters?.map( ( filterName ) => { - const filter = filterIndex[ filterName ]; + const filterComponents = filters?.map( ( filter ) => { + if ( ! filter.isVisible ) { + return null; + } - if ( ! filter ) { - return null; - } + if ( OPERATOR_IN === filter.operator ) { + return ( + + ); + } - if ( filter.type === 'enumeration' ) { - return ( - - ); - } + return null; + } ); - return null; - } ) || __( 'No filters available' ) + filterComponents.push( + ); + + if ( filterComponents.length > 1 ) { + filterComponents.push( + + ); + } + + return filterComponents; } diff --git a/packages/edit-site/src/components/dataviews/in-filter.js b/packages/edit-site/src/components/dataviews/in-filter.js index ca9436f5a7ea1f..4154e0576101c1 100644 --- a/packages/edit-site/src/components/dataviews/in-filter.js +++ b/packages/edit-site/src/components/dataviews/in-filter.js @@ -5,39 +5,57 @@ import { __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, SelectControl, } from '@wordpress/components'; -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { __, sprintf } from '@wordpress/i18n'; -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; - -const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); +export const OPERATOR_IN = 'in'; export default ( { filter, view, onChangeView } ) => { + const valueFound = view.filters.find( + ( f ) => f.field === filter.field && f.operator === OPERATOR_IN + ); + + const activeValue = + ! valueFound || ! valueFound.hasOwnProperty( 'value' ) + ? '' + : valueFound.value; + + const id = `dataviews__filters-in-${ filter.field }`; + return ( - { filter.name + ':' } + { sprintf( + /* translators: filter name. */ + __( '%s:' ), + filter.name + ) } } options={ filter.elements } onChange={ ( value ) => { - if ( value === '' ) { - value = undefined; - } + const filters = view.filters.filter( + ( f ) => + f.field !== filter.field || f.operator !== OPERATOR_IN + ); + + filters.push( { + field: filter.field, + operator: OPERATOR_IN, + value, + } ); onChangeView( ( currentView ) => ( { ...currentView, - filters: cleanEmptyObject( { - ...currentView.filters, - [ filter.id ]: value, - } ), + page: 1, + filters, } ) ); } } /> diff --git a/packages/edit-site/src/components/dataviews/index.js b/packages/edit-site/src/components/dataviews/index.js index 422d128b1461d0..eebdb77220c689 100644 --- a/packages/edit-site/src/components/dataviews/index.js +++ b/packages/edit-site/src/components/dataviews/index.js @@ -1 +1 @@ -export { default as DataViews } from './dataviews'; +export { default as DataViews, viewTypeSupportsMap } from './dataviews'; diff --git a/packages/edit-site/src/components/dataviews/item-actions.js b/packages/edit-site/src/components/dataviews/item-actions.js index 2b7837ae4a3829..777244e0396a70 100644 --- a/packages/edit-site/src/components/dataviews/item-actions.js +++ b/packages/edit-site/src/components/dataviews/item-actions.js @@ -6,12 +6,62 @@ import { MenuGroup, MenuItem, Button, + Modal, __experimentalHStack as HStack, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useMemo } from '@wordpress/element'; +import { useMemo, useState } from '@wordpress/element'; import { moreVertical } from '@wordpress/icons'; +function PrimaryActionTrigger( { action, onClick } ) { + return ( + + ); +}; diff --git a/packages/edit-site/src/components/dataviews/search.js b/packages/edit-site/src/components/dataviews/search.js index 3ade147922ac99..17a882637a7183 100644 --- a/packages/edit-site/src/components/dataviews/search.js +++ b/packages/edit-site/src/components/dataviews/search.js @@ -31,6 +31,7 @@ export default function Search( { label, view, onChangeView } ) { const searchLabel = label || __( 'Filter list' ); return ( view.type === v.id ); +function ViewTypeMenu( { view, onChangeView, supportedLayouts } ) { + let _availableViews = availableViews; + if ( supportedLayouts ) { + _availableViews = _availableViews.filter( ( _view ) => + supportedLayouts.includes( _view.id ) + ); + } + if ( _availableViews.length === 1 ) { + return null; + } + const activeView = _availableViews.find( ( v ) => view.type === v.id ); return ( } > - { availableViews.map( ( availableView ) => { + { _availableViews.map( ( availableView ) => { return ( + } > - + 0 && + header.column.columnDef.filters.some( + ( f ) => 'string' === typeof f && f === 'in' + ) + ) { + filter = { + field: header.column.columnDef.id, + elements: [ + { + value: '', + label: __( 'All' ), + }, + ...( header.column.columnDef.elements || [] ), + ], + }; + } + const isFilterable = !! filter; + return ( ) } + { isFilterable && ( + + } + suffix={ + + } + > + { __( 'Filter by' ) } + + } + > + { filter.elements.map( ( element ) => { + let isActive = false; + const columnFilters = + dataView.getState().columnFilters; + const columnFilter = columnFilters.find( + ( f ) => + Object.keys( f )[ 0 ].split( + ':' + )[ 0 ] === filter.field + ); + + // Set the empty item as active if the filter is not set. + if ( ! columnFilter && element.value === '' ) { + isActive = true; + } + + if ( columnFilter ) { + const value = + Object.values( columnFilter )[ 0 ]; + // Intentionally use loose comparison, so it does type conversion. + // This covers the case where a top-level filter for the same field converts a number into a string. + isActive = element.value == value; // eslint-disable-line eqeqeq + } + + return ( + + } + onSelect={ () => { + const otherFilters = + columnFilters?.filter( + ( f ) => { + const [ + field, + operator, + ] = + Object.keys( + f + )[ 0 ].split( ':' ); + return ( + field !== + filter.field || + operator !== 'in' + ); + } + ); + + if ( element.value === '' ) { + dataView.setColumnFilters( + otherFilters + ); + } else { + dataView.setColumnFilters( [ + ...otherFilters, + { + [ filter.field + + ':in' ]: element.value, + }, + ] ); + } + } } + > + { element.label } + + ); + } ) } + + + ) } ); @@ -156,7 +266,7 @@ function ViewList( { } ); if ( actions?.length ) { _columns.push( { - header: { __( 'Actions' ) }, + header: __( 'Actions' ), id: 'actions', cell: ( props ) => { return ( @@ -186,6 +296,58 @@ function ViewList( { ); }, [ view.hiddenFields ] ); + /** + * Transform the filters from the view format into the tanstack columns filter format. + * + * Input: + * + * view.filters = [ + * { field: 'date', operator: 'before', value: '2020-01-01' }, + * { field: 'date', operator: 'after', value: '2020-01-01' }, + * ] + * + * Output: + * + * columnFilters = [ + * { "date:before": '2020-01-01' }, + * { "date:after": '2020-01-01' } + * ] + * + * @param {Array} filters The view filters to transform. + * @return {Array} The transformed TanStack column filters. + */ + const toTanStackColumnFilters = ( filters ) => + filters?.map( ( filter ) => ( { + [ filter.field + ':' + filter.operator ]: filter.value, + } ) ); + + /** + * Transform the filters from the view format into the tanstack columns filter format. + * + * Input: + * + * columnFilters = [ + * { "date:before": '2020-01-01'}, + * { "date:after": '2020-01-01' } + * ] + * + * Output: + * + * view.filters = [ + * { field: 'date', operator: 'before', value: '2020-01-01' }, + * { field: 'date', operator: 'after', value: '2020-01-01' }, + * ] + * + * @param {Array} filters The TanStack column filters to transform. + * @return {Array} The transformed view filters. + */ + const fromTanStackColumnFilters = ( filters ) => + filters.map( ( filter ) => { + const [ key, value ] = Object.entries( filter )[ 0 ]; + const [ field, operator ] = key.split( ':' ); + return { field, operator, value }; + } ); + const dataView = useReactTable( { data, columns, @@ -203,6 +365,7 @@ function ViewList( { ] : [], globalFilter: view.search, + columnFilters: toTanStackColumnFilters( view.filters ), pagination: { pageIndex: view.page, pageSize: view.perPage, @@ -261,7 +424,14 @@ function ViewList( { } ); }, onGlobalFilterChange: ( value ) => { - onChangeView( { ...view, search: value, page: 0 } ); + onChangeView( { ...view, search: value, page: 1 } ); + }, + onColumnFiltersChange: ( columnFiltersUpdater ) => { + onChangeView( { + ...view, + filters: fromTanStackColumnFilters( columnFiltersUpdater() ), + page: 1, + } ); }, onPaginationChange: ( paginationUpdater ) => { onChangeView( ( currentView ) => { @@ -304,6 +474,7 @@ function ViewList( { header.column.columnDef .maxWidth || undefined, } } + data-field-id={ header.id } > ; +} diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index dcec39ddc7bedb..110e891cc1858a 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -6,31 +6,37 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { Notice } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; -import { EntityProvider } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; import { - BlockContextProvider, BlockBreadcrumb, store as blockEditorStore, privateApis as blockEditorPrivateApis, + BlockInspector, } from '@wordpress/block-editor'; import { InterfaceSkeleton, ComplementaryArea, store as interfaceStore, } from '@wordpress/interface'; -import { EditorNotices, EditorSnackbars } from '@wordpress/editor'; +import { + EditorNotices, + EditorSnackbars, + privateApis as editorPrivateApis, +} from '@wordpress/editor'; import { __, sprintf } from '@wordpress/i18n'; +import { store as coreDataStore } from '@wordpress/core-data'; +import { useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import { SidebarComplementaryAreaFills } from '../sidebar-edit-mode'; -import BlockEditor from '../block-editor'; +import { + SidebarComplementaryAreaFills, + SidebarInspectorFill, +} from '../sidebar-edit-mode'; import CodeEditor from '../code-editor'; import KeyboardShortcutsEditMode from '../keyboard-shortcuts/edit-mode'; import InserterSidebar from '../secondary-sidebar/inserter-sidebar'; @@ -46,8 +52,13 @@ import useEditedEntityRecord from '../use-edited-entity-record'; import { SidebarFixedBottomSlot } from '../sidebar-edit-mode/sidebar-fixed-bottom'; import PatternModal from '../pattern-modal'; import { POST_TYPE_LABELS, TEMPLATE_POST_TYPE } from '../../utils/constants'; +import SiteEditorCanvas from '../block-editor/site-editor-canvas'; +import TemplatePartConverter from '../template-part-converter'; +import { useSpecificEditorSettings } from '../block-editor/use-site-editor-settings'; const { BlockRemovalWarningModal } = unlock( blockEditorPrivateApis ); +const { ExperimentalEditorProvider: EditorProvider } = + unlock( editorPrivateApis ); const interfaceLabels = { /* translators: accessibility text for the editor content landmark region. */ @@ -79,10 +90,11 @@ export default function Editor( { listViewToggleElement, isLoading } ) { isLoaded: hasLoadedPost, } = useEditedEntityRecord(); - const { id: editedPostId, type: editedPostType } = editedPost; + const { type: editedPostType } = editedPost; const { context, + contextPost, editorMode, canvasMode, blockEditorMode, @@ -92,6 +104,7 @@ export default function Editor( { listViewToggleElement, isLoading } ) { showIconLabels, showBlockBreadcrumbs, hasPageContentFocus, + pageContentFocusType, } = useSelect( ( select ) => { const { getEditedPostContext, @@ -100,14 +113,24 @@ export default function Editor( { listViewToggleElement, isLoading } ) { isInserterOpened, isListViewOpened, hasPageContentFocus: _hasPageContentFocus, + getPageContentFocusType, } = unlock( select( editSiteStore ) ); const { __unstableGetEditorMode } = select( blockEditorStore ); const { getActiveComplementaryArea } = select( interfaceStore ); + const { getEntityRecord } = select( coreDataStore ); + const _context = getEditedPostContext(); // The currently selected entity to display. // Typically template or template part in the site editor. return { - context: getEditedPostContext(), + context: _context, + contextPost: _context?.postId + ? getEntityRecord( + 'postType', + _context.postType, + _context.postId + ) + : undefined, editorMode: getEditorMode(), canvasMode: getCanvasMode(), blockEditorMode: __unstableGetEditorMode(), @@ -125,9 +148,9 @@ export default function Editor( { listViewToggleElement, isLoading } ) { 'showBlockBreadcrumbs' ), hasPageContentFocus: _hasPageContentFocus(), + pageContentFocusType: getPageContentFocusType(), }; }, [] ); - const { setEditedPostContext } = useDispatch( editSiteStore ); const isViewMode = canvasMode === 'view'; const isEditMode = canvasMode === 'edit'; @@ -142,23 +165,7 @@ export default function Editor( { listViewToggleElement, isLoading } ) { const secondarySidebarLabel = isListViewOpen ? __( 'List View' ) : __( 'Block Library' ); - const blockContext = useMemo( () => { - const { postType, postId, ...nonPostFields } = context ?? {}; - return { - ...( hasPageContentFocus ? context : nonPostFields ), - queryContext: [ - context?.queryContext || { page: 1 }, - ( newQueryContext ) => - setEditedPostContext( { - ...context, - queryContext: { - ...context?.queryContext, - ...newQueryContext, - }, - } ), - ], - }; - }, [ hasPageContentFocus, context, setEditedPostContext ] ); + const postWithTemplate = context?.postId; let title; if ( hasLoadedPost ) { @@ -180,110 +187,131 @@ export default function Editor( { listViewToggleElement, isLoading } ) { 'edit-site-editor__loading-progress' ); - const contentProps = isLoading - ? { - 'aria-busy': 'true', - 'aria-describedby': loadingProgressId, - } - : undefined; + const settings = useSpecificEditorSettings(); + const isReady = + ! isLoading && + ( ( postWithTemplate && !! contextPost && !! editedPost ) || + ( ! postWithTemplate && !! editedPost ) ); + const mode = useMemo( () => { + if ( isViewMode ) { + return postWithTemplate ? 'template-locked' : 'all'; + } + + if ( isEditMode && pageContentFocusType === 'hideTemplate' ) { + return 'post-only'; + } + + if ( postWithTemplate && hasPageContentFocus ) { + return 'template-locked'; + } + + if ( postWithTemplate && ! hasPageContentFocus ) { + return 'template-only'; + } + + return 'all'; + }, [ + isViewMode, + isEditMode, + postWithTemplate, + pageContentFocusType, + hasPageContentFocus, + ] ); return ( <> - { isLoading ? : null } + { ! isReady ? : null } { isEditMode && } - - + { __( + "You attempted to edit an item that doesn't exist. Perhaps it was deleted?" + ) } + + ) } + { isReady && ( + - - - { isEditMode && } - } - content={ - <> - - { isEditMode && } - { showVisualEditor && editedPost && ( - <> - - - - - ) } - { editorMode === 'text' && - editedPost && - isEditMode && } - { hasLoadedPost && ! editedPost && ( - - { __( - "You attempted to edit an item that doesn't exist. Perhaps it was deleted?" - ) } - - ) } - { isEditMode && ( - - ) } - - } - contentProps={ contentProps } - secondarySidebar={ - isEditMode && - ( ( shouldShowInserter && ( - - ) ) || - ( shouldShowListView && ( - - ) ) ) + + { isEditMode && } + } + content={ + <> + + { isEditMode && } + { showVisualEditor && ( <> - - + + + + + + + - ) - } - footer={ - shouldShowBlockBreadcrumbs && ( - + ) } + { isEditMode && } + + } + secondarySidebar={ + isEditMode && + ( ( shouldShowInserter && ) || + ( shouldShowListView && ( + - ) - } - labels={ { - ...interfaceLabels, - secondarySidebar: secondarySidebarLabel, - } } - /> - - - + ) ) ) + } + sidebar={ + isEditMode && + isRightSidebarOpen && ( + <> + + + + ) + } + footer={ + shouldShowBlockBreadcrumbs && ( + + ) + } + labels={ { + ...interfaceLabels, + secondarySidebar: secondarySidebarLabel, + } } + /> + + ) } ); } diff --git a/packages/edit-site/src/components/global-styles-renderer/index.js b/packages/edit-site/src/components/global-styles-renderer/index.js index eca6d9b2662e8f..83c7c51bbe366b 100644 --- a/packages/edit-site/src/components/global-styles-renderer/index.js +++ b/packages/edit-site/src/components/global-styles-renderer/index.js @@ -32,7 +32,7 @@ function useGlobalStylesRenderer() { styles: [ ...nonGlobalStyles, ...styles ], __experimentalFeatures: settings, } ); - }, [ styles, settings ] ); + }, [ styles, settings, updateSettings, getSettings ] ); } export function GlobalStylesRenderer() { diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index 46a7a8c74ab692..d5c884f9d20cfe 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -32,8 +32,8 @@ const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( ); function ScreenRevisions() { - const { goBack } = useNavigator(); - const { user: userConfig, setUserConfig } = + const { goTo } = useNavigator(); + const { user: currentEditorGlobalStyles, setUserConfig } = useContext( GlobalStylesContext ); const { blocks, editorCanvasContainerView } = useSelect( ( select ) => { return { @@ -45,9 +45,8 @@ function ScreenRevisions() { }, [] ); const { revisions, isLoading, hasUnsavedChanges } = useGlobalStylesRevisions(); - const [ selectedRevisionId, setSelectedRevisionId ] = useState(); - const [ globalStylesRevision, setGlobalStylesRevision ] = - useState( userConfig ); + const [ currentlySelectedRevision, setCurrentlySelectedRevision ] = + useState( currentEditorGlobalStyles ); const [ isLoadingRevisionWithUnsavedChanges, setIsLoadingRevisionWithUnsavedChanges, @@ -55,16 +54,13 @@ function ScreenRevisions() { const { setEditorCanvasContainerView } = unlock( useDispatch( editSiteStore ) ); - - useEffect( () => { - if ( editorCanvasContainerView !== 'global-styles-revisions' ) { - goBack(); - setEditorCanvasContainerView( editorCanvasContainerView ); - } - }, [ editorCanvasContainerView ] ); + const selectedRevisionMatchesEditorStyles = areGlobalStyleConfigsEqual( + currentlySelectedRevision, + currentEditorGlobalStyles + ); const onCloseRevisions = () => { - goBack(); + goTo( '/' ); // Return to global styles main panel. }; const restoreRevision = ( revision ) => { @@ -77,17 +73,47 @@ function ScreenRevisions() { }; const selectRevision = ( revision ) => { - setGlobalStylesRevision( { + setCurrentlySelectedRevision( { styles: revision?.styles || {}, settings: revision?.settings || {}, id: revision?.id, } ); - setSelectedRevisionId( revision?.id ); }; + useEffect( () => { + if ( editorCanvasContainerView !== 'global-styles-revisions' ) { + goTo( '/' ); // Return to global styles main panel. + setEditorCanvasContainerView( editorCanvasContainerView ); + } + }, [ editorCanvasContainerView ] ); + + const firstRevision = revisions[ 0 ]; + const currentlySelectedRevisionId = currentlySelectedRevision?.id; + const shouldSelectFirstItem = + !! firstRevision?.id && + ! selectedRevisionMatchesEditorStyles && + ! currentlySelectedRevisionId; + + useEffect( () => { + /* + * Ensure that the first item is selected and loaded into the preview pane + * when no revision is selected and the selected styles don't match the current editor styles. + * This is required in case editor styles are changed outside the revisions panel, + * e.g., via the reset styles function of useGlobalStylesReset(). + * See: https://github.com/WordPress/gutenberg/issues/55866 + */ + if ( shouldSelectFirstItem ) { + setCurrentlySelectedRevision( { + styles: firstRevision?.styles || {}, + settings: firstRevision?.settings || {}, + id: firstRevision?.id, + } ); + } + }, [ shouldSelectFirstItem, firstRevision ] ); + + // Only display load button if there is a revision to load and it is different from the current editor styles. const isLoadButtonEnabled = - !! globalStylesRevision?.id && - ! areGlobalStyleConfigsEqual( globalStylesRevision, userConfig ); + !! currentlySelectedRevisionId && ! selectedRevisionMatchesEditorStyles; const shouldShowRevisions = ! isLoading && revisions.length; return ( @@ -95,7 +121,7 @@ function ScreenRevisions() { { isLoading && ( @@ -105,13 +131,13 @@ function ScreenRevisions() { <>
    { isLoadButtonEnabled && ( @@ -120,8 +146,9 @@ function ScreenRevisions() { variant="primary" className="edit-site-global-styles-screen-revisions__button" disabled={ - ! globalStylesRevision?.id || - globalStylesRevision?.id === 'unsaved' + ! currentlySelectedRevisionId || + currentlySelectedRevisionId === + 'unsaved' } onClick={ () => { if ( hasUnsavedChanges ) { @@ -130,12 +157,12 @@ function ScreenRevisions() { ); } else { restoreRevision( - globalStylesRevision + currentlySelectedRevision ); } } } > - { globalStylesRevision?.id === 'parent' + { currentlySelectedRevisionId === 'parent' ? __( 'Reset to defaults' ) : __( 'Apply' ) } @@ -147,7 +174,7 @@ function ScreenRevisions() { isOpen={ isLoadingRevisionWithUnsavedChanges } confirmButtonText={ __( 'Apply' ) } onConfirm={ () => - restoreRevision( globalStylesRevision ) + restoreRevision( currentlySelectedRevision ) } onCancel={ () => setIsLoadingRevisionWithUnsavedChanges( false ) diff --git a/packages/edit-site/src/components/global-styles/screen-root.js b/packages/edit-site/src/components/global-styles/screen-root.js index 1b0ce6b002a0bb..28b3642ac09b45 100644 --- a/packages/edit-site/src/components/global-styles/screen-root.js +++ b/packages/edit-site/src/components/global-styles/screen-root.js @@ -27,8 +27,9 @@ import RootMenu from './root-menu'; import StylesPreview from './preview'; import { unlock } from '../../lock-unlock'; +const { useGlobalStyle } = unlock( blockEditorPrivateApis ); + function ScreenRoot() { - const { useGlobalStyle } = unlock( blockEditorPrivateApis ); const [ customCSS ] = useGlobalStyle( 'css' ); const { hasVariations, canEditCSS } = useSelect( ( select ) => { diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index d2e0d2c3027065..2e33d4b599b7b9 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -50,6 +50,7 @@ import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; const SLOT_FILL_NAME = 'GlobalStylesMenu'; +const { useGlobalStylesReset } = unlock( blockEditorPrivateApis ); const { Slot: GlobalStylesMenuSlot, Fill: GlobalStylesMenuFill } = createSlotFill( SLOT_FILL_NAME ); @@ -127,7 +128,6 @@ function GlobalStylesRevisionsMenu() { globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0, }; }, [] ); - const { useGlobalStylesReset } = unlock( blockEditorPrivateApis ); const [ canReset, onReset ] = useGlobalStylesReset(); const { goTo } = useNavigator(); const { setEditorCanvasContainerView } = unlock( diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js new file mode 100644 index 00000000000000..dda761c983d31d --- /dev/null +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -0,0 +1,201 @@ +/** + * WordPress dependencies + */ +import { useCallback, useRef } from '@wordpress/element'; +import { useViewportMatch } from '@wordpress/compose'; +import { + ToolSelector, + NavigableToolbar, + store as blockEditorStore, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { _x, __ } from '@wordpress/i18n'; +import { listView, plus, chevronUpDown } from '@wordpress/icons'; +import { Button, ToolbarItem } from '@wordpress/components'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; + +/** + * Internal dependencies + */ +import UndoButton from '../undo-redo/undo'; +import RedoButton from '../undo-redo/redo'; +import { store as editSiteStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; + +const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); + +const preventDefault = ( event ) => { + event.preventDefault(); +}; + +export default function DocumentTools( { + blockEditorMode, + hasFixedToolbar, + isDistractionFree, + showIconLabels, + setListViewToggleElement, +} ) { + const inserterButton = useRef(); + const { isInserterOpen, isListViewOpen, listViewShortcut, isVisualMode } = + useSelect( ( select ) => { + const { + __experimentalGetPreviewDeviceType, + isInserterOpened, + isListViewOpened, + getEditorMode, + } = select( editSiteStore ); + const { getShortcutRepresentation } = select( + keyboardShortcutsStore + ); + + return { + deviceType: __experimentalGetPreviewDeviceType(), + isInserterOpen: isInserterOpened(), + isListViewOpen: isListViewOpened(), + listViewShortcut: getShortcutRepresentation( + 'core/edit-site/toggle-list-view' + ), + isVisualMode: getEditorMode() === 'visual', + }; + }, [] ); + + const { + __experimentalSetPreviewDeviceType: setPreviewDeviceType, + setIsInserterOpened, + setIsListViewOpened, + } = useDispatch( editSiteStore ); + const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + + const isLargeViewport = useViewportMatch( 'medium' ); + + const toggleInserter = useCallback( () => { + if ( isInserterOpen ) { + // Focusing the inserter button should close the inserter popover. + // However, there are some cases it won't close when the focus is lost. + // See https://github.com/WordPress/gutenberg/issues/43090 for more details. + inserterButton.current.focus(); + setIsInserterOpened( false ); + } else { + setIsInserterOpened( true ); + } + }, [ isInserterOpen, setIsInserterOpened ] ); + + const toggleListView = useCallback( + () => setIsListViewOpened( ! isListViewOpen ), + [ setIsListViewOpened, isListViewOpen ] + ); + + const { + shouldShowContextualToolbar, + canFocusHiddenToolbar, + fixedToolbarCanBeFocused, + } = useShouldContextualToolbarShow(); + // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. + // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. + const blockToolbarCanBeFocused = + shouldShowContextualToolbar || + canFocusHiddenToolbar || + fixedToolbarCanBeFocused; + + /* translators: button label text should, if possible, be under 16 characters. */ + const longLabel = _x( + 'Toggle block inserter', + 'Generic label for block inserter button' + ); + const shortLabel = ! isInserterOpen ? __( 'Add' ) : __( 'Close' ); + + const isZoomedOutViewExperimentEnabled = + window?.__experimentalEnableZoomedOutView && isVisualMode; + const isZoomedOutView = blockEditorMode === 'zoom-out'; + + return ( + +
    + { ! isDistractionFree && ( + + ) } + { isLargeViewport && ( + <> + { ! hasFixedToolbar && ( + + ) } + + + { ! isDistractionFree && ( + + ) } + { isZoomedOutViewExperimentEnabled && + ! isDistractionFree && + ! hasFixedToolbar && ( + { + setPreviewDeviceType( 'Desktop' ); + __unstableSetEditorMode( + isZoomedOutView + ? 'edit' + : 'zoom-out' + ); + } } + /> + ) } + + ) } +
    +
    + ); +} diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index 3528e0623fc7d5..a7daf5570a204b 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -6,29 +6,26 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useCallback, useRef } from '@wordpress/element'; import { useViewportMatch, useReducedMotion } from '@wordpress/compose'; import { store as coreStore } from '@wordpress/core-data'; import { - ToolSelector, __experimentalPreviewOptions as PreviewOptions, - NavigableToolbar, - store as blockEditorStore, privateApis as blockEditorPrivateApis, + store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; +import { useEffect, useRef, useState } from '@wordpress/element'; import { PinnedItems } from '@wordpress/interface'; -import { _x, __ } from '@wordpress/i18n'; -import { listView, plus, external, chevronUpDown } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { external, next, previous } from '@wordpress/icons'; import { - __unstableMotion as motion, Button, - ToolbarItem, + __unstableMotion as motion, MenuGroup, MenuItem, + Popover, VisuallyHidden, } from '@wordpress/components'; -import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as preferencesStore } from '@wordpress/preferences'; /** @@ -36,9 +33,8 @@ import { store as preferencesStore } from '@wordpress/preferences'; */ import MoreMenu from './more-menu'; import SaveButton from '../save-button'; -import UndoButton from './undo-redo/undo'; -import RedoButton from './undo-redo/redo'; import DocumentActions from './document-actions'; +import DocumentTools from './document-tools'; import { store as editSiteStore } from '../../store'; import { getEditorCanvasContainerTitle, @@ -47,37 +43,25 @@ import { import { unlock } from '../../lock-unlock'; import { FOCUSABLE_ENTITIES } from '../../utils/constants'; -const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); - -const preventDefault = ( event ) => { - event.preventDefault(); -}; +const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); export default function HeaderEditMode( { setListViewToggleElement } ) { - const inserterButton = useRef(); const { deviceType, templateType, - isInserterOpen, - isListViewOpen, - listViewShortcut, - isVisualMode, isDistractionFree, blockEditorMode, + blockSelectionStart, homeUrl, showIconLabels, editorCanvasView, hasFixedToolbar, + isZoomOutMode, } = useSelect( ( select ) => { - const { - __experimentalGetPreviewDeviceType, - getEditedPostType, - isInserterOpened, - isListViewOpened, - getEditorMode, - } = select( editSiteStore ); - const { getShortcutRepresentation } = select( keyboardShortcutsStore ); - const { __unstableGetEditorMode } = select( blockEditorStore ); + const { __experimentalGetPreviewDeviceType, getEditedPostType } = + select( editSiteStore ); + const { getBlockSelectionStart, __unstableGetEditorMode } = + select( blockEditorStore ); const postType = getEditedPostType(); @@ -90,13 +74,8 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { return { deviceType: __experimentalGetPreviewDeviceType(), templateType: postType, - isInserterOpen: isInserterOpened(), - isListViewOpen: isListViewOpened(), - listViewShortcut: getShortcutRepresentation( - 'core/edit-site/toggle-list-view' - ), - isVisualMode: getEditorMode() === 'visual', blockEditorMode: __unstableGetEditorMode(), + blockSelectionStart: getBlockSelectionStart(), homeUrl: getUnstableBase()?.home, showIconLabels: getPreference( editSiteStore.name, @@ -105,71 +84,44 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { editorCanvasView: unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), - isDistractionFree: getPreference( - editSiteStore.name, - 'distractionFree' - ), hasFixedToolbar: getPreference( editSiteStore.name, 'fixedToolbar' ), + isDistractionFree: getPreference( + editSiteStore.name, + 'distractionFree' + ), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); - const { - __experimentalSetPreviewDeviceType: setPreviewDeviceType, - setIsInserterOpened, - setIsListViewOpened, - } = useDispatch( editSiteStore ); - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); - const disableMotion = useReducedMotion(); - const isLargeViewport = useViewportMatch( 'medium' ); + const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport; + const blockToolbarRef = useRef(); - const toggleInserter = useCallback( () => { - if ( isInserterOpen ) { - // Focusing the inserter button should close the inserter popover. - // However, there are some cases it won't close when the focus is lost. - // See https://github.com/WordPress/gutenberg/issues/43090 for more details. - inserterButton.current.focus(); - setIsInserterOpened( false ); - } else { - setIsInserterOpened( true ); - } - }, [ isInserterOpen, setIsInserterOpened ] ); - - const toggleListView = useCallback( - () => setIsListViewOpened( ! isListViewOpen ), - [ setIsListViewOpened, isListViewOpen ] - ); - - const { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - } = useShouldContextualToolbarShow(); - // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. - // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. - const blockToolbarCanBeFocused = - shouldShowContextualToolbar || - canFocusHiddenToolbar || - fixedToolbarCanBeFocused; + const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } = + useDispatch( editSiteStore ); + const disableMotion = useReducedMotion(); const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer(); const isFocusMode = FOCUSABLE_ENTITIES.includes( templateType ); - /* translators: button label text should, if possible, be under 16 characters. */ - const longLabel = _x( - 'Toggle block inserter', - 'Generic label for block inserter button' - ); - const shortLabel = ! isInserterOpen ? __( 'Add' ) : __( 'Close' ); - - const isZoomedOutViewExperimentEnabled = - window?.__experimentalEnableZoomedOutView && isVisualMode; const isZoomedOutView = blockEditorMode === 'zoom-out'; + const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = + useState( true ); + + const hasBlockSelected = !! blockSelectionStart; + + useEffect( () => { + // If we have a new block selection, show the block tools + if ( blockSelectionStart ) { + setIsBlockToolsCollapsed( false ); + } + }, [ blockSelectionStart ] ); + const toolbarVariants = { isDistractionFree: { y: '-50px' }, isDistractionFreeHovering: { y: 0 }, @@ -190,116 +142,66 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { } ) } > { hasDefaultEditorCanvasView && ( - -
    - { ! isDistractionFree && ( - - ) } - { isLargeViewport && ( - <> - { ! hasFixedToolbar && ( - + + { isTopToolbar && ( + <> +
    + +
    + + { hasBlockSelected && ( +
    -
    + ) } + + ) } + ) } { ! isDistractionFree && ( -
    +
    { ! hasDefaultEditorCanvasView ? ( getEditorCanvasContainerTitle( editorCanvasView ) ) : ( diff --git a/packages/edit-site/src/components/header-edit-mode/more-menu/site-export.js b/packages/edit-site/src/components/header-edit-mode/more-menu/site-export.js index ec9492081a42b3..85ec4f0dd7335e 100644 --- a/packages/edit-site/src/components/header-edit-mode/more-menu/site-export.js +++ b/packages/edit-site/src/components/header-edit-mode/more-menu/site-export.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import downloadjs from 'downloadjs'; - /** * WordPress dependencies */ @@ -11,6 +6,7 @@ import { MenuItem } from '@wordpress/components'; import apiFetch from '@wordpress/api-fetch'; import { download } from '@wordpress/icons'; import { useDispatch } from '@wordpress/data'; +import { downloadBlob } from '@wordpress/blob'; import { store as noticesStore } from '@wordpress/notices'; export default function SiteExport() { @@ -35,7 +31,7 @@ export default function SiteExport() { ? contentDispositionMatches[ 1 ] : 'edit-site-export'; - downloadjs( blob, fileName + '.zip', 'application/zip' ); + downloadBlob( fileName + '.zip', blob, 'application/zip' ); } catch ( errorResponse ) { let error = {}; try { diff --git a/packages/edit-site/src/components/header-edit-mode/style.scss b/packages/edit-site/src/components/header-edit-mode/style.scss index 26b1716a28b865..78a988b2716ae0 100644 --- a/packages/edit-site/src/components/header-edit-mode/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/style.scss @@ -15,6 +15,17 @@ $header-toolbar-min-width: 335px; .edit-site-header-edit-mode__start { display: flex; border: none; + align-items: center; + flex-shrink: 2; + // We need this to be overflow hidden so the block toolbar can + // overflow scroll. If the overflow is visible, flexbox allows + // the toolbar to grow outside of the allowed container space. + overflow: hidden; + // Take up the full height of the header so the border focus + // is visible on toolbar buttons. + height: 100%; + // Allow focus ring to be fully visible on furthest right button. + padding-right: 2px; } .edit-site-header-edit-mode__end { @@ -35,6 +46,10 @@ $header-toolbar-min-width: 335px; // See https://dev.w3.org/csswg/css-flexbox/#min-size-auto min-width: 0; } + + .block-editor-block-contextual-toolbar.is-fixed { + border: none; + } } .edit-site-header-edit-mode__toolbar { @@ -99,7 +114,7 @@ $header-toolbar-min-width: 335px; } } -.edit-site-header-edit-mode__start { +.edit-site-header-edit-mode__document-tools { display: flex; border: none; @@ -183,7 +198,25 @@ $header-toolbar-min-width: 335px; padding: 0 $grid-unit-10; } - .edit-site-header-edit-mode__start .edit-site-header-edit-mode__toolbar > * + * { + .edit-site-header-edit-mode__document-tools .edit-site-header-edit-mode__toolbar > * + * { margin-left: $grid-unit-10; } } + +.has-fixed-toolbar { + .selected-block-tools-wrapper { + overflow-x: scroll; + + &.is-collapsed { + display: none; + } + } + + .edit-site-header-edit-mode__center.is-collapsed { + display: none; + } +} + +.edit-site-header-edit-mode__block-tools-toggle { + margin-left: 2px; // Allow focus ring to be fully visible +} diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 5fb682353da722..9e4153938d40ad 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -18,7 +18,7 @@ import { useResizeObserver, } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; -import { useState, useRef } from '@wordpress/element'; +import { useState } from '@wordpress/element'; import { NavigableRegion } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { @@ -72,7 +72,6 @@ export default function Layout() { useCommonCommands(); useBlockCommands(); - const hubRef = useRef(); const { params } = useLocation(); const isMobileViewport = useViewportMatch( 'medium', '<' ); const isListPage = getIsListPage( params, isMobileViewport ); @@ -117,7 +116,7 @@ export default function Layout() { } ); const disableMotion = useReducedMotion(); const showSidebar = - ( isMobileViewport && ! isListPage ) || + ( isMobileViewport && canvasMode === 'view' && ! isListPage ) || ( ! isMobileViewport && ( canvasMode === 'view' || ! isEditorPage ) ); const showCanvas = ( isMobileViewport && isEditorPage && isEditing ) || @@ -193,6 +192,7 @@ export default function Layout() { 'is-full-canvas': isFullCanvas, 'is-edit-mode': isEditing, 'has-fixed-toolbar': hasFixedToolbar, + 'is-block-toolbar-visible': hasBlockSelected, } ) } > @@ -226,13 +226,6 @@ export default function Layout() { animate={ headerAnimationState } > @@ -290,7 +283,7 @@ export default function Layout() { // (https://github.com/WordPress/gutenberg/pull/51558/files#r1231763003), // so we can't remove the element entirely. Using `inert` will make // it inaccessible to screen readers and keyboard navigation. - inert={ showSidebar ? undefined : 'inert' } + inert={ showSidebar ? undefined : 'true' } animate={ { opacity: showSidebar ? 1 : 0 } } transition={ { type: 'tween', diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 11c7bdeeaf2a19..3bea97862b1c4c 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -157,11 +157,16 @@ } // This shouldn't be necessary (we should have a way to say that a skeletton is relative -.edit-site-layout__canvas .interface-interface-skeleton { +.edit-site-layout__canvas .interface-interface-skeleton, +.edit-site-page-pages-preview .interface-interface-skeleton { position: relative !important; min-height: 100% !important; } +.edit-site-page-pages-preview { + height: 100%; +} + .edit-site-layout__view-mode-toggle.components-button { position: relative; color: $white; @@ -249,23 +254,6 @@ } } -.edit-site-layout.has-fixed-toolbar { - // making the header be lower than the content - // so the fixed toolbar can be positioned on top of it - // but only on desktop - @include break-medium() { - .edit-site-layout__canvas-container { - z-index: 5; - } - .edit-site-site-hub { - z-index: 4; - } - .edit-site-layout__header:focus-within { - z-index: 3; - } - } -} - .is-edit-mode.is-distraction-free { .edit-site-layout__header-container { diff --git a/packages/edit-site/src/components/list/added-by.js b/packages/edit-site/src/components/list/added-by.js index da6111e0b29bd5..e9c8df0fa7f263 100644 --- a/packages/edit-site/src/components/list/added-by.js +++ b/packages/edit-site/src/components/list/added-by.js @@ -152,7 +152,7 @@ export function useAddedBy( postType, postId ) { * @param {Object} props * @param {string} props.imageUrl */ -function AvatarImage( { imageUrl } ) { +export function AvatarImage( { imageUrl } ) { const [ isImageLoaded, setIsImageLoaded ] = useState( false ); return ( diff --git a/packages/edit-site/src/components/page-content-focus-manager/back-to-page-notification.js b/packages/edit-site/src/components/page-content-focus-manager/back-to-page-notification.js index a2990c56a673cf..9bf9ac33b1d198 100644 --- a/packages/edit-site/src/components/page-content-focus-manager/back-to-page-notification.js +++ b/packages/edit-site/src/components/page-content-focus-manager/back-to-page-notification.js @@ -25,27 +25,19 @@ export default function BackToPageNotification() { * switches from focusing on editing page content to editing a template. */ export function useBackToPageNotification() { - const { isPage, hasPageContentFocus } = useSelect( - ( select ) => ( { - isPage: select( editSiteStore ).isPage(), - hasPageContentFocus: select( editSiteStore ).hasPageContentFocus(), - } ), + const hasPageContentFocus = useSelect( + ( select ) => select( editSiteStore ).hasPageContentFocus(), [] ); + const { isPage } = useSelect( editSiteStore ); const alreadySeen = useRef( false ); - const prevHasPageContentFocus = useRef( false ); const { createInfoNotice } = useDispatch( noticesStore ); const { setHasPageContentFocus } = useDispatch( editSiteStore ); useEffect( () => { - if ( - ! alreadySeen.current && - isPage && - prevHasPageContentFocus.current && - ! hasPageContentFocus - ) { + if ( isPage() && ! alreadySeen.current && ! hasPageContentFocus ) { createInfoNotice( __( 'You are editing a template.' ), { isDismissible: true, type: 'snackbar', @@ -58,11 +50,8 @@ export function useBackToPageNotification() { } ); alreadySeen.current = true; } - prevHasPageContentFocus.current = hasPageContentFocus; }, [ - alreadySeen, isPage, - prevHasPageContentFocus, hasPageContentFocus, createInfoNotice, setHasPageContentFocus, diff --git a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js b/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js index f3e021ba885244..2f81f80d0ce63d 100644 --- a/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js +++ b/packages/edit-site/src/components/page-content-focus-manager/disable-non-page-content-blocks.js @@ -1,9 +1,11 @@ /** * WordPress dependencies */ -import { createHigherOrderComponent } from '@wordpress/compose'; -import { addFilter, removeFilter } from '@wordpress/hooks'; -import { useBlockEditingMode } from '@wordpress/block-editor'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { + useBlockEditingMode, + store as blockEditorStore, +} from '@wordpress/block-editor'; import { useEffect } from '@wordpress/element'; /** @@ -11,42 +13,45 @@ import { useEffect } from '@wordpress/element'; */ import { PAGE_CONTENT_BLOCK_TYPES } from '../../utils/constants'; +function DisableBlock( { clientId } ) { + const isDescendentOfQueryLoop = useSelect( + ( select ) => { + const { getBlockParentsByBlockName } = select( blockEditorStore ); + return ( + getBlockParentsByBlockName( clientId, 'core/query' ).length !== + 0 + ); + }, + [ clientId ] + ); + const mode = isDescendentOfQueryLoop ? undefined : 'contentOnly'; + const { setBlockEditingMode, unsetBlockEditingMode } = + useDispatch( blockEditorStore ); + useEffect( () => { + if ( mode ) { + setBlockEditingMode( clientId, mode ); + return () => { + unsetBlockEditingMode( clientId ); + }; + } + }, [ clientId, mode, setBlockEditingMode, unsetBlockEditingMode ] ); +} + /** * Component that when rendered, makes it so that the site editor allows only * page content to be edited. */ export default function DisableNonPageContentBlocks() { - useDisableNonPageContentBlocks(); - return null; -} - -/** - * Disables non-content blocks using the `useBlockEditingMode` hook. - */ -export function useDisableNonPageContentBlocks() { useBlockEditingMode( 'disabled' ); - useEffect( () => { - addFilter( - 'editor.BlockEdit', - 'core/edit-site/disable-non-content-blocks', - withDisableNonPageContentBlocks + const clientIds = useSelect( ( select ) => { + const { __experimentalGetGlobalBlocksByName } = + select( blockEditorStore ); + return __experimentalGetGlobalBlocksByName( + Object.keys( PAGE_CONTENT_BLOCK_TYPES ) ); - return () => - removeFilter( - 'editor.BlockEdit', - 'core/edit-site/disable-non-content-blocks' - ); }, [] ); -} -const withDisableNonPageContentBlocks = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const isDescendentOfQueryLoop = props.context.queryId !== undefined; - const isPageContent = - PAGE_CONTENT_BLOCK_TYPES[ props.name ] && ! isDescendentOfQueryLoop; - const mode = isPageContent ? 'contentOnly' : undefined; - useBlockEditingMode( mode ); - return ; - }, - 'withDisableNonPageContentBlocks' -); + return clientIds.map( ( clientId ) => { + return ; + } ); +} diff --git a/packages/edit-site/src/components/page-main/index.js b/packages/edit-site/src/components/page-main/index.js index 10b5b99dc2fbf5..e2b39e0ecd151d 100644 --- a/packages/edit-site/src/components/page-main/index.js +++ b/packages/edit-site/src/components/page-main/index.js @@ -9,6 +9,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import PagePatterns from '../page-patterns'; import PageTemplateParts from '../page-template-parts'; import PageTemplates from '../page-templates'; +import DataviewsTemplates from '../page-templates/dataviews-templates'; import PagePages from '../page-pages'; import { unlock } from '../../lock-unlock'; @@ -20,7 +21,11 @@ export default function PageMain() { } = useLocation(); if ( path === '/wp_template/all' ) { - return ; + return window?.__experimentalAdminViews ? ( + + ) : ( + + ); } else if ( path === '/wp_template_part/all' ) { return ; } else if ( path === '/patterns' ) { diff --git a/packages/edit-site/src/components/page-pages/default-views.js b/packages/edit-site/src/components/page-pages/default-views.js deleted file mode 100644 index a9849d83b0226b..00000000000000 --- a/packages/edit-site/src/components/page-pages/default-views.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { trash } from '@wordpress/icons'; - -// DEFAULT_STATUSES is intentionally sorted. Items do not have spaces in between them. -// The reason for that is to match the default statuses coming from the endpoint -// (entity request and useEffect to update the view). -export const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All statuses but 'trash'. - -const DEFAULT_PAGE_BASE = { - type: 'list', - search: '', - filters: { - status: DEFAULT_STATUSES, - }, - page: 1, - perPage: 5, - sort: { - field: 'date', - direction: 'desc', - }, - visibleFilters: [ 'author', 'status' ], - // All fields are visible by default, so it's - // better to keep track of the hidden ones. - hiddenFields: [ 'date', 'featured-image' ], - layout: {}, -}; - -const DEFAULT_VIEWS = [ - { - title: __( 'All' ), - slug: 'all', - view: DEFAULT_PAGE_BASE, - }, - { - title: __( 'Drafts' ), - slug: 'drafts', - view: { - ...DEFAULT_PAGE_BASE, - filters: { - status: 'draft', - }, - }, - }, - { - title: __( 'Trash' ), - slug: 'trash', - icon: trash, - view: { - ...DEFAULT_PAGE_BASE, - filters: { - status: 'trash', - }, - }, - }, -]; - -export default DEFAULT_VIEWS; diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 47761ce55d1132..c2461adc34f8d0 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -6,25 +6,29 @@ import { __experimentalVStack as VStack, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useEntityRecords } from '@wordpress/core-data'; +import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { dateI18n, getDate, getSettings } from '@wordpress/date'; import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies */ import Page from '../page'; import Link from '../routes/link'; -import { DataViews } from '../dataviews'; -import { DEFAULT_STATUSES, default as DEFAULT_VIEWS } from './default-views'; +import { DataViews, viewTypeSupportsMap } from '../dataviews'; +import { default as DEFAULT_VIEWS } from '../sidebar-dataviews/default-views'; import { useTrashPostAction, + usePermanentlyDeletePostAction, + useRestorePostAction, postRevisionsAction, viewPostAction, useEditPostAction, } from '../actions'; +import SideEditor from './side-editor'; import Media from '../media'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); @@ -37,73 +41,117 @@ const defaultConfigPerViewType = { }, }; -export default function PagePages() { +function useView( type ) { const { - params: { path, activeView = 'all' }, + params: { activeView = 'all', isCustom = 'false' }, } = useLocation(); - const initialView = DEFAULT_VIEWS.find( - ( { slug } ) => slug === activeView - ).view; - const [ view, setView ] = useState( initialView ); + const selectedDefaultView = + isCustom === 'false' && + DEFAULT_VIEWS[ type ].find( ( { slug } ) => slug === activeView )?.view; + const [ view, setView ] = useState( selectedDefaultView ); + useEffect( () => { - setView( - DEFAULT_VIEWS.find( ( { slug } ) => slug === activeView ).view + if ( selectedDefaultView ) { + setView( selectedDefaultView ); + } + }, [ selectedDefaultView ] ); + const editedViewRecord = useSelect( + ( select ) => { + if ( isCustom !== 'true' ) { + return; + } + const { getEditedEntityRecord } = select( coreStore ); + const dataviewRecord = getEditedEntityRecord( + 'postType', + 'wp_dataviews', + Number( activeView ) + ); + return dataviewRecord; + }, + [ activeView, isCustom ] + ); + const { editEntityRecord } = useDispatch( coreStore ); + + const customView = useMemo( () => { + return ( + editedViewRecord?.content && JSON.parse( editedViewRecord?.content ) ); - }, [ path, activeView ] ); - // Request post statuses to get the proper labels. - const { records: statuses } = useEntityRecords( 'root', 'status' ); - const defaultStatuses = useMemo( () => { - return statuses === null - ? DEFAULT_STATUSES - : statuses - .filter( ( { slug } ) => slug !== 'trash' ) - .map( ( { slug } ) => slug ) - .sort() - .join(); - }, [ statuses ] ); + }, [ editedViewRecord?.content ] ); + const setCustomView = useCallback( + ( viewToSet ) => { + editEntityRecord( + 'postType', + 'wp_dataviews', + editedViewRecord?.id, + { + content: JSON.stringify( viewToSet ), + } + ); + }, + [ editEntityRecord, editedViewRecord?.id ] + ); - useEffect( () => { - // Only update the view if the statuses received from the endpoint - // are different from the DEFAULT_STATUSES provided initially. - // - // The pages endpoint depends on the status endpoint via the status filter. - // Initially, this code filters the pages request by DEFAULT_STATUTES, - // instead of using the default (publish). - // https://developer.wordpress.org/rest-api/reference/pages/#list-pages - // - // By doing so, it avoids a second request to the pages endpoint - // upon receiving the statuses when they are the same (most common scenario). - if ( DEFAULT_STATUSES !== defaultStatuses ) { - setView( { - ...view, - filters: { - ...view.filters, - status: defaultStatuses, - }, - } ); + if ( isCustom === 'false' ) { + return [ view, setView ]; + } else if ( isCustom === 'true' && customView ) { + return [ customView, setCustomView ]; + } + // Loading state where no the view was not found on custom views or default views. + return [ DEFAULT_VIEWS[ type ][ 0 ].view, setView ]; +} + +// See https://github.com/WordPress/gutenberg/issues/55886 +// We do not support custom statutes at the moment. +const STATUSES = [ + { value: 'draft', label: __( 'Draft' ) }, + { value: 'future', label: __( 'Scheduled' ) }, + { value: 'pending', label: __( 'Pending Review' ) }, + { value: 'private', label: __( 'Private' ) }, + { value: 'publish', label: __( 'Published' ) }, + { value: 'trash', label: __( 'Trash' ) }, +]; +const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All but 'trash'. + +export default function PagePages() { + const postType = 'page'; + const [ view, setView ] = useView( postType ); + const [ selection, setSelection ] = useState( [] ); + + const queryArgs = useMemo( () => { + const filters = {}; + view.filters.forEach( ( filter ) => { + if ( filter.field === 'status' && filter.operator === 'in' ) { + filters.status = filter.value; + } + if ( filter.field === 'author' && filter.operator === 'in' ) { + filters.author = filter.value; + } + } ); + // We want to provide a different default item for the status filter + // than the REST API provides. + if ( ! filters.status || filters.status === '' ) { + filters.status = DEFAULT_STATUSES; } - }, [ defaultStatuses ] ); - const queryArgs = useMemo( - () => ( { + return { per_page: view.perPage, page: view.page, _embed: 'author', order: view.sort?.direction, orderby: view.sort?.field, search: view.search, - ...view.filters, - } ), - [ view ] - ); + ...filters, + }; + }, [ view ] ); const { records: pages, isResolving: isLoadingPages, totalItems, totalPages, - } = useEntityRecords( 'postType', 'page', queryArgs ); + } = useEntityRecords( 'postType', postType, queryArgs ); - const { records: authors } = useEntityRecords( 'root', 'user' ); + const { records: authors, isResolving: isLoadingAuthors } = + useEntityRecords( 'root', 'user' ); const paginationInfo = useMemo( () => ( { @@ -137,7 +185,7 @@ export default function PagePages() { header: __( 'Title' ), id: 'title', getValue: ( { item } ) => item.title?.rendered || item.slug, - render: ( { item } ) => { + render: ( { item, view: { type } } ) => { return ( @@ -147,6 +195,14 @@ export default function PagePages() { postType: item.type, canvas: 'edit', } } + onClick={ ( event ) => { + if ( + viewTypeSupportsMap[ type ].preview + ) { + event.preventDefault(); + setSelection( [ item.id ] ); + } + } } > { decodeEntities( item.title?.rendered || item.slug @@ -157,7 +213,6 @@ export default function PagePages() { ); }, maxWidth: 400, - sortingFn: 'alphanumeric', enableHiding: false, }, { @@ -172,7 +227,7 @@ export default function PagePages() { ); }, - filters: [ 'enumeration' ], + filters: [ 'in' ], elements: authors?.map( ( { id, name } ) => ( { value: id, @@ -183,20 +238,10 @@ export default function PagePages() { header: __( 'Status' ), id: 'status', getValue: ( { item } ) => - statuses?.find( ( { slug } ) => slug === item.status ) - ?.name ?? item.status, - filters: [ - { - type: 'enumeration', - id: 'status', - resetValue: defaultStatuses, - }, - ], - elements: - statuses?.map( ( { slug, name } ) => ( { - value: slug, - label: name, - } ) ) || [], + STATUSES.find( ( { value } ) => value === item.status ) + ?.label ?? item.status, + filters: [ 'in' ], + elements: STATUSES, enableSorting: false, }, { @@ -212,19 +257,28 @@ export default function PagePages() { }, }, ], - [ defaultStatuses, statuses, authors ] + [ authors ] ); const trashPostAction = useTrashPostAction(); + const permanentlyDeletePostAction = usePermanentlyDeletePostAction(); + const restorePostAction = useRestorePostAction(); const editPostAction = useEditPostAction(); const actions = useMemo( () => [ viewPostAction, trashPostAction, + restorePostAction, + permanentlyDeletePostAction, editPostAction, postRevisionsAction, ], - [ trashPostAction, editPostAction ] + [ + trashPostAction, + permanentlyDeletePostAction, + restorePostAction, + editPostAction, + ] ); const onChangeView = useCallback( ( viewUpdater ) => { @@ -248,16 +302,43 @@ export default function PagePages() { // TODO: we need to handle properly `data={ data || EMPTY_ARRAY }` for when `isLoading`. return ( - - - + <> + + + + { viewTypeSupportsMap[ view.type ].preview && ( + +
    + { selection.length === 1 && ( + + ) } + { selection.length !== 1 && ( +
    +

    { __( 'Select a page to preview' ) }

    +
    + ) } +
    +
    + ) } + ); } diff --git a/packages/edit-site/src/components/page-pages/side-editor.js b/packages/edit-site/src/components/page-pages/side-editor.js new file mode 100644 index 00000000000000..fca561cf9f4d5d --- /dev/null +++ b/packages/edit-site/src/components/page-pages/side-editor.js @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import Editor from '../editor'; +import { useInitEditedEntity } from '../sync-state-with-url/use-init-edited-entity-from-url'; + +export default function SideEditor( { postType, postId } ) { + useInitEditedEntity( { + postId, + postType, + } ); + + return ; +} diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js index 873f71f2f108d1..b394ef8eb6e76c 100644 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import downloadjs from 'downloadjs'; import { paramCase as kebabCase } from 'change-case'; /** @@ -37,6 +36,7 @@ import { } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; +import { downloadBlob } from '@wordpress/blob'; /** * Internal dependencies @@ -118,9 +118,9 @@ function GridItem( { categoryId, item, ...props } ) { syncStatus: item.patternBlock.wp_pattern_sync_status, }; - return downloadjs( - JSON.stringify( json, null, 2 ), + return downloadBlob( `${ kebabCase( item.title || item.name ) }.json`, + JSON.stringify( json, null, 2 ), 'application/json' ); }; diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index bbcb7e7910211a..eb56fdded90607 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -12,7 +12,7 @@ import { __experimentalHeading as Heading, __experimentalText as Text, } from '@wordpress/components'; -import { __, isRTL } from '@wordpress/i18n'; +import { __, _x, isRTL } from '@wordpress/i18n'; import { chevronLeft, chevronRight } from '@wordpress/icons'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useAsyncList, useViewportMatch } from '@wordpress/compose'; @@ -33,9 +33,15 @@ import Pagination from './pagination'; const { useLocation, useHistory } = unlock( routerPrivateApis ); const SYNC_FILTERS = { - all: __( 'All' ), - [ PATTERN_SYNC_TYPES.full ]: __( 'Synced' ), - [ PATTERN_SYNC_TYPES.unsynced ]: __( 'Not synced' ), + all: _x( 'All', 'Option that shows all patterns' ), + [ PATTERN_SYNC_TYPES.full ]: _x( + 'Synced', + 'Option that shows all synchronized patterns' + ), + [ PATTERN_SYNC_TYPES.unsynced ]: _x( + 'Not synced', + 'Option that shows all patterns that are not synchronized' + ), }; const SYNC_DESCRIPTIONS = { diff --git a/packages/edit-site/src/components/page-patterns/use-patterns.js b/packages/edit-site/src/components/page-patterns/use-patterns.js index fde4eaadb5dc05..9cb6c8b998c412 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -17,7 +17,7 @@ import { decodeEntities } from '@wordpress/html-entities'; */ import { filterOutDuplicatesByName } from './utils'; import { - PATTERN_CORE_SOURCES, + EXCLUDED_PATTERN_SOURCES, PATTERN_TYPES, PATTERN_SYNC_TYPES, TEMPLATE_PART_POST_TYPE, @@ -123,7 +123,8 @@ const selectThemePatterns = createSelector( ...( restBlockPatterns || [] ), ] .filter( - ( pattern ) => ! PATTERN_CORE_SOURCES.includes( pattern.source ) + ( pattern ) => + ! EXCLUDED_PATTERN_SOURCES.includes( pattern.source ) ) .filter( filterOutDuplicatesByName ) .filter( ( pattern ) => pattern.inserter !== false ) diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js new file mode 100644 index 00000000000000..ce4927895b29be --- /dev/null +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -0,0 +1,224 @@ +/** + * External dependencies + */ +import removeAccents from 'remove-accents'; + +/** + * WordPress dependencies + */ +import { + Icon, + __experimentalHeading as Heading, + __experimentalText as Text, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { __, _x } from '@wordpress/i18n'; +import { useState, useMemo, useCallback } from '@wordpress/element'; +import { useEntityRecords } from '@wordpress/core-data'; +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Internal dependencies + */ +import Page from '../page'; +import Link from '../routes/link'; +import { useAddedBy, AvatarImage } from '../list/added-by'; +import { TEMPLATE_POST_TYPE } from '../../utils/constants'; +import { DataViews } from '../dataviews'; +import { + useResetTemplateAction, + deleteTemplateAction, + renameTemplateAction, +} from './template-actions'; + +const EMPTY_ARRAY = []; + +const DEFAULT_VIEW = { + type: 'list', + search: '', + page: 1, + perPage: 20, + // All fields are visible by default, so it's + // better to keep track of the hidden ones. + hiddenFields: [], + layout: {}, +}; + +function normalizeSearchInput( input = '' ) { + return removeAccents( input.trim().toLowerCase() ); +} + +// TODO: these are going to be reused in the template part list. +// That's the reason for leaving the template parts code for now. +function TemplateTitle( { item } ) { + const { isCustomized } = useAddedBy( item.type, item.id ); + return ( + + + + { decodeEntities( item.title?.rendered || item.slug ) || + __( '(no title)' ) } + + + { isCustomized && ( + + { item.type === TEMPLATE_POST_TYPE + ? _x( 'Customized', 'template' ) + : _x( 'Customized', 'template part' ) } + + ) } + + ); +} + +function AuthorField( { item } ) { + const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); + return ( + + { imageUrl ? ( + + ) : ( +
    + +
    + ) } + { text } +
    + ); +} + +export default function DataviewsTemplates() { + const [ view, setView ] = useState( DEFAULT_VIEW ); + const { records: allTemplates, isResolving: isLoadingData } = + useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { + per_page: -1, + } ); + const { shownTemplates, paginationInfo } = useMemo( () => { + if ( ! allTemplates ) { + return { + shownTemplates: EMPTY_ARRAY, + paginationInfo: { totalItems: 0, totalPages: 0 }, + }; + } + let filteredTemplates = [ ...allTemplates ]; + // Handle global search. + if ( view.search ) { + const normalizedSearch = normalizeSearchInput( view.search ); + filteredTemplates = filteredTemplates.filter( ( item ) => { + const title = item.title?.rendered || item.slug; + return ( + normalizeSearchInput( title ).includes( + normalizedSearch + ) || + normalizeSearchInput( item.description ).includes( + normalizedSearch + ) + ); + } ); + } + // Handle sorting. + // TODO: Explore how this can be more dynamic.. + if ( view.sort ) { + if ( view.sort.field === 'title' ) { + filteredTemplates.sort( ( a, b ) => { + const titleA = a.title?.rendered || a.slug; + const titleB = b.title?.rendered || b.slug; + return view.sort.direction === 'asc' + ? titleA.localeCompare( titleB ) + : titleB.localeCompare( titleA ); + } ); + } + } + // Handle pagination. + const start = ( view.page - 1 ) * view.perPage; + const totalItems = filteredTemplates?.length || 0; + filteredTemplates = filteredTemplates?.slice( + start, + start + view.perPage + ); + return { + shownTemplates: filteredTemplates, + paginationInfo: { + totalItems, + totalPages: Math.ceil( totalItems / view.perPage ), + }, + }; + }, [ allTemplates, view ] ); + const fields = useMemo( + () => [ + { + header: __( 'Template' ), + id: 'title', + getValue: ( { item } ) => item.title?.rendered || item.slug, + render: ( { item } ) => , + maxWidth: 400, + enableHiding: false, + }, + { + header: __( 'Description' ), + id: 'description', + getValue: ( { item } ) => item.description, + render: ( { item } ) => { + return ( + item.description && ( + + { decodeEntities( item.description ) } + + ) + ); + }, + maxWidth: 200, + enableSorting: false, + }, + { + header: __( 'Author' ), + id: 'author', + getValue: () => {}, + render: ( { item } ) => , + enableHiding: false, + enableSorting: false, + }, + ], + [] + ); + const resetTemplateAction = useResetTemplateAction(); + const actions = useMemo( + () => [ + resetTemplateAction, + deleteTemplateAction, + renameTemplateAction, + ], + [ resetTemplateAction ] + ); + const onChangeView = useCallback( + ( viewUpdater ) => { + const updatedView = + typeof viewUpdater === 'function' + ? viewUpdater( view ) + : viewUpdater; + setView( updatedView ); + }, + [ view, setView ] + ); + return ( + + + + ); +} diff --git a/packages/edit-site/src/components/page-templates/template-actions.js b/packages/edit-site/src/components/page-templates/template-actions.js new file mode 100644 index 00000000000000..9f5897e31fb93e --- /dev/null +++ b/packages/edit-site/src/components/page-templates/template-actions.js @@ -0,0 +1,209 @@ +/** + * WordPress dependencies + */ +import { backup, trash } from '@wordpress/icons'; +import { __, sprintf } from '@wordpress/i18n'; +import { useDispatch } from '@wordpress/data'; +import { useMemo, useState } from '@wordpress/element'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as noticesStore } from '@wordpress/notices'; +import { decodeEntities } from '@wordpress/html-entities'; +import { + Button, + TextControl, + __experimentalText as Text, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../store'; +import isTemplateRevertable from '../../utils/is-template-revertable'; +import isTemplateRemovable from '../../utils/is-template-removable'; +import { TEMPLATE_POST_TYPE } from '../../utils/constants'; + +export function useResetTemplateAction() { + const { revertTemplate } = useDispatch( editSiteStore ); + const { saveEditedEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + return useMemo( + () => ( { + id: 'reset-template', + label: __( 'Reset template' ), + isPrimary: true, + icon: backup, + isEligible: isTemplateRevertable, + async callback( template ) { + try { + await revertTemplate( template, { allowUndo: false } ); + await saveEditedEntityRecord( + 'postType', + template.type, + template.id + ); + + createSuccessNotice( + sprintf( + /* translators: The template/part's name. */ + __( '"%s" reverted.' ), + decodeEntities( template.title.rendered ) + ), + { + type: 'snackbar', + id: 'edit-site-template-reverted', + } + ); + } catch ( error ) { + const fallbackErrorMessage = + template.type === TEMPLATE_POST_TYPE + ? __( + 'An error occurred while reverting the template.' + ) + : __( + 'An error occurred while reverting the template part.' + ); + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : fallbackErrorMessage; + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + }, + } ), + [ + createErrorNotice, + createSuccessNotice, + revertTemplate, + saveEditedEntityRecord, + ] + ); +} + +export const deleteTemplateAction = { + id: 'delete-template', + label: __( 'Delete template' ), + isPrimary: true, + icon: trash, + isEligible: isTemplateRemovable, + hideModalHeader: true, + RenderModal: ( { item: template, closeModal } ) => { + const { removeTemplate } = useDispatch( editSiteStore ); + return ( + + + { sprintf( + // translators: %s: The template or template part's title. + __( 'Are you sure you want to delete "%s"?' ), + decodeEntities( template.title.rendered ) + ) } + + + + + + + ); + }, +}; + +export const renameTemplateAction = { + id: 'rename-template', + label: __( 'Rename' ), + isEligible: ( template ) => + isTemplateRemovable( template ) && template.is_custom, + RenderModal: ( { item: template, closeModal } ) => { + const title = decodeEntities( template.title.rendered ); + const [ editedTitle, setEditedTitle ] = useState( title ); + const { + editEntityRecord, + __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits, + } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + async function onTemplateRename( event ) { + event.preventDefault(); + try { + await editEntityRecord( + 'postType', + template.type, + template.id, + { + title: editedTitle, + } + ); + // Update state before saving rerenders the list. + setEditedTitle( '' ); + closeModal(); + // Persist edited entity. + await saveSpecifiedEntityEdits( + 'postType', + template.type, + template.id, + [ 'title' ], // Only save title to avoid persisting other edits. + { + throwOnError: true, + } + ); + // TODO: this action will be reused in template parts list, so + // let's keep this for a bit, even it's always a `template` now. + createSuccessNotice( + template.type === TEMPLATE_POST_TYPE + ? __( 'Template renamed.' ) + : __( 'Template part renamed.' ), + { + type: 'snackbar', + } + ); + } catch ( error ) { + const fallbackErrorMessage = + template.type === TEMPLATE_POST_TYPE + ? __( 'An error occurred while renaming the template.' ) + : __( + 'An error occurred while renaming the template part.' + ); + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : fallbackErrorMessage; + + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + } + return ( +
    + + + + + + + +
    + ); + }, +}; diff --git a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js new file mode 100644 index 00000000000000..d84d85faf4d60a --- /dev/null +++ b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js @@ -0,0 +1,141 @@ +/** + * WordPress dependencies + */ +import { + Modal, + TextControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + Button, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useDispatch, resolveSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useState } from '@wordpress/element'; +import { plus } from '@wordpress/icons'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import SidebarNavigationItem from '../sidebar-navigation-item'; +import DEFAULT_VIEWS from './default-views'; +import { unlock } from '../../lock-unlock'; + +const { useHistory, useLocation } = unlock( routerPrivateApis ); + +function AddNewItemModalContent( { type, setIsAdding } ) { + const { + params: { path }, + } = useLocation(); + const history = useHistory(); + const { saveEntityRecord } = useDispatch( coreStore ); + const [ title, setTitle ] = useState( '' ); + const [ isSaving, setIsSaving ] = useState( false ); + return ( +
    { + event.preventDefault(); + setIsSaving( true ); + const { getEntityRecords } = resolveSelect( coreStore ); + let dataViewTaxonomyId; + const dataViewTypeRecords = await getEntityRecords( + 'taxonomy', + 'wp_dataviews_type', + { slug: type } + ); + if ( dataViewTypeRecords && dataViewTypeRecords.length > 0 ) { + dataViewTaxonomyId = dataViewTypeRecords[ 0 ].id; + } else { + const record = await saveEntityRecord( + 'taxonomy', + 'wp_dataviews_type', + { name: type } + ); + if ( record && record.id ) { + dataViewTaxonomyId = record.id; + } + } + const savedRecord = await saveEntityRecord( + 'postType', + 'wp_dataviews', + { + title, + status: 'publish', + wp_dataviews_type: dataViewTaxonomyId, + content: JSON.stringify( + DEFAULT_VIEWS[ type ][ 0 ].view + ), + } + ); + history.push( { + path, + activeView: savedRecord.id, + isCustom: 'true', + } ); + setIsSaving( false ); + setIsAdding( false ); + } } + > + + + + + + + + +
    + ); +} + +export default function AddNewItem( { type } ) { + const [ isAdding, setIsAdding ] = useState( false ); + return ( + <> + { + setIsAdding( true ); + } } + className="dataviews__siderbar-content-add-new-item" + > + { __( 'New view' ) } + + { isAdding && ( + { + setIsAdding( false ); + } } + > + + + ) } + + ); +} diff --git a/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js b/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js new file mode 100644 index 00000000000000..12659a36bbf9b8 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js @@ -0,0 +1,227 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { + __experimentalItemGroup as ItemGroup, + __experimentalHeading as Heading, + DropdownMenu, + MenuGroup, + MenuItem, + TextControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + Button, + Modal, +} from '@wordpress/components'; +import { useMemo, useState } from '@wordpress/element'; +import { moreVertical } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import DataViewItem from './dataview-item'; +import AddNewItem from './add-new-view'; +import { unlock } from '../../lock-unlock'; + +const { useHistory, useLocation } = unlock( routerPrivateApis ); + +const EMPTY_ARRAY = []; + +function RenameItemModalContent( { dataviewId, currentTitle, setIsRenaming } ) { + const { editEntityRecord } = useDispatch( coreStore ); + const [ title, setTitle ] = useState( currentTitle ); + return ( +
    { + event.preventDefault(); + await editEntityRecord( + 'postType', + 'wp_dataviews', + dataviewId, + { + title, + } + ); + setIsRenaming( false ); + } } + > + + + + + + + +
    + ); +} + +function CustomDataViewItem( { dataviewId, isActive } ) { + const { + params: { path }, + } = useLocation(); + const history = useHistory(); + const { dataview } = useSelect( + ( select ) => { + const { getEditedEntityRecord } = select( coreStore ); + return { + dataview: getEditedEntityRecord( + 'postType', + 'wp_dataviews', + dataviewId + ), + }; + }, + [ dataviewId ] + ); + const { deleteEntityRecord } = useDispatch( coreStore ); + const type = useMemo( () => { + const viewContent = JSON.parse( dataview.content ); + return viewContent.type; + }, [ dataview.content ] ); + const [ isRenaming, setIsRenaming ] = useState( false ); + return ( + <> + + { ( { onClose } ) => ( + + { + setIsRenaming( true ); + onClose(); + } } + > + { __( 'Rename' ) } + + { + await deleteEntityRecord( + 'postType', + 'wp_dataviews', + dataview.id, + { + force: true, + } + ); + if ( isActive ) { + history.replace( { + path, + } ); + } + onClose(); + } } + isDestructive + > + { __( 'Delete' ) } + + + ) } + + } + /> + { isRenaming && ( + { + setIsRenaming( false ); + } } + > + + + ) } + + ); +} + +export function useCustomDataViews( type ) { + const customDataViews = useSelect( ( select ) => { + const { getEntityRecords } = select( coreStore ); + const dataViewTypeRecords = getEntityRecords( + 'taxonomy', + 'wp_dataviews_type', + { slug: type } + ); + if ( ! dataViewTypeRecords || dataViewTypeRecords.length === 0 ) { + return EMPTY_ARRAY; + } + const dataViews = getEntityRecords( 'postType', 'wp_dataviews', { + wp_dataviews_type: dataViewTypeRecords[ 0 ].id, + orderby: 'date', + order: 'asc', + } ); + if ( ! dataViews ) { + return EMPTY_ARRAY; + } + return dataViews; + } ); + return customDataViews; +} + +export default function CustomDataViewsList( { type, activeView, isCustom } ) { + const customDataViews = useCustomDataViews( type ); + return ( + <> +
    + { __( 'Custom Views' ) } +
    + + { customDataViews.map( ( customViewRecord ) => { + return ( + + ); + } ) } + + + + ); +} diff --git a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js new file mode 100644 index 00000000000000..c6d7bbe4a231ba --- /dev/null +++ b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { page, columns, pullRight } from '@wordpress/icons'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { __experimentalHStack as HStack } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { useLink } from '../routes/link'; +import SidebarNavigationItem from '../sidebar-navigation-item'; +import { unlock } from '../../lock-unlock'; +const { useLocation } = unlock( routerPrivateApis ); + +function getDataViewIcon( type ) { + const icons = { list: page, grid: columns, 'side-by-side': pullRight }; + return icons[ type ]; +} + +export default function DataViewItem( { + title, + slug, + customViewId, + type, + icon, + isActive, + isCustom, + suffix, +} ) { + const { + params: { path }, + } = useLocation(); + + const iconToUse = icon || getDataViewIcon( type ); + + const linkInfo = useLink( { + path, + activeView: isCustom === 'true' ? customViewId : slug, + isCustom, + } ); + return ( + + + { title } + + { suffix } + + ); +} diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js new file mode 100644 index 00000000000000..a1557e3b8a3445 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -0,0 +1,54 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { trash } from '@wordpress/icons'; + +const DEFAULT_PAGE_BASE = { + type: 'list', + search: '', + filters: [], + page: 1, + perPage: 20, + sort: { + field: 'date', + direction: 'desc', + }, + // All fields are visible by default, so it's + // better to keep track of the hidden ones. + hiddenFields: [ 'date', 'featured-image' ], + layout: {}, +}; + +const DEFAULT_VIEWS = { + page: [ + { + title: __( 'All' ), + slug: 'all', + view: DEFAULT_PAGE_BASE, + }, + { + title: __( 'Drafts' ), + slug: 'drafts', + view: { + ...DEFAULT_PAGE_BASE, + filters: [ + { field: 'status', operator: 'in', value: 'draft' }, + ], + }, + }, + { + title: __( 'Trash' ), + slug: 'trash', + icon: trash, + view: { + ...DEFAULT_PAGE_BASE, + filters: [ + { field: 'status', operator: 'in', value: 'trash' }, + ], + }, + }, + ], +}; + +export default DEFAULT_VIEWS; diff --git a/packages/edit-site/src/components/sidebar-dataviews/index.js b/packages/edit-site/src/components/sidebar-dataviews/index.js index 7704c0637b4904..9e4534ab342745 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/index.js +++ b/packages/edit-site/src/components/sidebar-dataviews/index.js @@ -2,65 +2,56 @@ * WordPress dependencies */ import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; -import { page, columns } from '@wordpress/icons'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import { useLink } from '../routes/link'; -import { default as DEFAULT_VIEWS } from '../page-pages/default-views'; + +import { default as DEFAULT_VIEWS } from './default-views'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); -import SidebarNavigationItem from '../sidebar-navigation-item'; - -function getDataViewIcon( dataview ) { - const icons = { list: page, grid: columns }; - return icons[ dataview.view.type ]; -} - -function DataViewItem( { dataview, isActive, icon } ) { - const { - params: { path }, - } = useLocation(); - - const _icon = icon || getDataViewIcon( dataview ); +import DataViewItem from './dataview-item'; +import CustomDataViewsList from './custom-dataviews-list'; - const linkInfo = useLink( { - path, - activeView: dataview.slug, - } ); - return ( - - { dataview.title } - - ); -} +const PATH_TO_TYPE = { + '/pages': 'page', +}; export default function DataViewsSidebarContent() { const { - params: { path, activeView = 'all' }, + params: { path, activeView = 'all', isCustom = 'false' }, } = useLocation(); - if ( ! path || path !== '/pages' ) { + if ( ! path || ! PATH_TO_TYPE[ path ] ) { return null; } + const type = PATH_TO_TYPE[ path ]; return ( - - { DEFAULT_VIEWS.map( ( dataview ) => { - return ( - - ); - } ) } - + <> + + { DEFAULT_VIEWS[ type ].map( ( dataview ) => { + return ( + + ); + } ) } + + + ); } diff --git a/packages/edit-site/src/components/sidebar-dataviews/style.scss b/packages/edit-site/src/components/sidebar-dataviews/style.scss new file mode 100644 index 00000000000000..526670ee1e5627 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-dataviews/style.scss @@ -0,0 +1,22 @@ +.edit-site-sidebar-navigation-screen-dataviews__group-header { + margin-top: $grid-unit-40; + h2 { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + } +} + +.edit-site-sidebar-dataviews-dataview-item { + &:hover, + &:focus, + &[aria-current] { + color: $gray-200; + background: $gray-800; + } + + &.is-selected { + background: var(--wp-admin-theme-color); + color: $white; + } +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/default-sidebar.js b/packages/edit-site/src/components/sidebar-edit-mode/default-sidebar.js index a06077e30b1761..48a59935bf17c2 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/default-sidebar.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/default-sidebar.js @@ -6,11 +6,7 @@ import { ComplementaryAreaMoreMenuItem, } from '@wordpress/interface'; import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../store'; +import { store as preferencesStore } from '@wordpress/preferences'; export default function DefaultSidebar( { className, @@ -24,7 +20,11 @@ export default function DefaultSidebar( { panelClassName, } ) { const showIconLabels = useSelect( - ( select ) => select( editSiteStore ).getSettings().showIconLabels, + ( select ) => + !! select( preferencesStore ).get( + 'core/edit-site', + 'showIconLabels' + ), [] ); diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js index 8067453a518217..1ee382420a7d9a 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js @@ -11,8 +11,9 @@ import { __experimentalText as Text, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { store as coreStore, useEntityBlockEditor } from '@wordpress/core-data'; +import { store as coreStore } from '@wordpress/core-data'; import { check } from '@wordpress/icons'; +import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies @@ -21,7 +22,7 @@ import { store as editSiteStore } from '../../../store'; import SwapTemplateButton from './swap-template-button'; import ResetDefaultTemplate from './reset-default-template'; import { unlock } from '../../../lock-unlock'; -import usePageContentBlocks from '../../block-editor/block-editor-provider/use-page-content-blocks'; +import { PAGE_CONTENT_BLOCK_TYPES } from '../../../utils/constants'; const POPOVER_PROPS = { className: 'edit-site-page-panels-edit-template__dropdown', @@ -29,8 +30,8 @@ const POPOVER_PROPS = { }; export default function EditTemplate() { - const { hasResolved, template, isTemplateHidden, postType } = useSelect( - ( select ) => { + const { hasPostContentBlocks, hasResolved, template, isTemplateHidden } = + useSelect( ( select ) => { const { getEditedPostContext, getEditedPostType, getEditedPostId } = select( editSiteStore ); const { getCanvasMode, getPageContentFocusType } = unlock( @@ -38,15 +39,15 @@ export default function EditTemplate() { ); const { getEditedEntityRecord, hasFinishedResolution } = select( coreStore ); + const { __experimentalGetGlobalBlocksByName } = + select( blockEditorStore ); const _context = getEditedPostContext(); const _postType = getEditedPostType(); - const queryArgs = [ - 'postType', - getEditedPostType(), - getEditedPostId(), - ]; - + const queryArgs = [ 'postType', _postType, getEditedPostId() ]; return { + hasPostContentBlocks: !! __experimentalGetGlobalBlocksByName( + Object.keys( PAGE_CONTENT_BLOCK_TYPES ) + ).length, context: _context, hasResolved: hasFinishedResolution( 'getEditedEntityRecord', @@ -58,21 +59,12 @@ export default function EditTemplate() { getPageContentFocusType() === 'hideTemplate', postType: _postType, }; - }, - [] - ); - - const [ blocks ] = useEntityBlockEditor( 'postType', postType ); + }, [] ); const { setHasPageContentFocus } = useDispatch( editSiteStore ); // Disable reason: `useDispatch` can't be called conditionally. // eslint-disable-next-line @wordpress/no-unused-vars-before-return const { setPageContentFocusType } = unlock( useDispatch( editSiteStore ) ); - // Check if there are any post content block types in the blocks tree. - const pageContentBlocks = usePageContentBlocks( { - blocks, - isPageContentFocused: true, - } ); if ( ! hasResolved ) { return null; @@ -108,12 +100,13 @@ export default function EditTemplate() { - { !! pageContentBlocks?.length && ( + { hasPostContentBlocks && ( { setPageContentFocusType( isTemplateHidden diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js index c4dafeab6cb372..26fa86c933f115 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js @@ -2,6 +2,8 @@ * WordPress dependencies */ import { __experimentalVStack as VStack } from '@wordpress/components'; +import { PostURLPanel } from '@wordpress/editor'; + /** * Internal dependencies */ @@ -32,6 +34,7 @@ export default function PageSummary( { postType={ postType } /> + ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js index fab050c62d2194..0f29292274546b 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js @@ -1,10 +1,10 @@ /** * WordPress dependencies */ -import { useDispatch } from '@wordpress/data'; import { MenuGroup, MenuItem } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useEntityRecord } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -14,14 +14,12 @@ import { useEditedPostContext, useIsPostsPage, } from './hooks'; -import { store as editSiteStore } from '../../../store'; export default function ResetDefaultTemplate( { onClick } ) { const currentTemplateSlug = useCurrentTemplateSlug(); const isPostsPage = useIsPostsPage(); const { postType, postId } = useEditedPostContext(); - const entity = useEntityRecord( 'postType', postType, postId ); - const { setPage } = useDispatch( editSiteStore ); + const { editEntityRecord } = useDispatch( coreStore ); // The default template in a post is indicated by an empty string. if ( ! currentTemplateSlug || isPostsPage ) { return null; @@ -30,11 +28,14 @@ export default function ResetDefaultTemplate( { onClick } ) { { - entity.edit( { template: '' }, { undoIgnore: true } ); + editEntityRecord( + 'postType', + postType, + postId, + { template: '' }, + { undoIgnore: true } + ); onClick(); - await setPage( { - context: { postType, postId }, - } ); } } > { __( 'Use default template' ) } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss index 5501fe49e5876b..acaf5cbfe35dde 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -84,4 +84,9 @@ .components-popover__content { min-width: 240px; } + .components-button.is-pressed, + .components-button.is-pressed:hover { + background: inherit; + color: inherit; + } } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js index fee4f22a3ae2bc..40eb1c5c4bd627 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js @@ -1,20 +1,19 @@ /** * WordPress dependencies */ -import { useDispatch } from '@wordpress/data'; import { useMemo, useState, useCallback } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor'; import { MenuItem, Modal } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useEntityRecord } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; import { parse } from '@wordpress/blocks'; import { useAsyncList } from '@wordpress/compose'; /** * Internal dependencies */ -import { store as editSiteStore } from '../../../store'; import { useAvailableTemplates, useEditedPostContext } from './hooks'; export default function SwapTemplateButton( { onClick } ) { @@ -24,16 +23,18 @@ export default function SwapTemplateButton( { onClick } ) { setShowModal( false ); }, [] ); const { postType, postId } = useEditedPostContext(); - const entitiy = useEntityRecord( 'postType', postType, postId ); - const { setPage } = useDispatch( editSiteStore ); + const { editEntityRecord } = useDispatch( coreStore ); if ( ! availableTemplates?.length ) { return null; } const onTemplateSelect = async ( template ) => { - entitiy.edit( { template: template.name }, { undoIgnore: true } ); - await setPage( { - context: { postType, postId }, - } ); + editEntityRecord( + 'postType', + postType, + postId, + { template: template.name }, + { undoIgnore: true } + ); onClose(); // Close the template suggestions modal first. onClick(); }; diff --git a/packages/edit-site/src/components/sidebar-edit-mode/plugin-sidebar/index.js b/packages/edit-site/src/components/sidebar-edit-mode/plugin-sidebar/index.js index d705d0724af9f7..45a3f6d48d0b98 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/plugin-sidebar/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/plugin-sidebar/index.js @@ -3,11 +3,7 @@ */ import { ComplementaryArea } from '@wordpress/interface'; import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../../store'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Renders a sidebar when activated. The contents within the `PluginSidebar` will appear as content within the sidebar. @@ -76,7 +72,11 @@ import { store as editSiteStore } from '../../../store'; */ export default function PluginSidebarEditSite( { className, ...props } ) { const showIconLabels = useSelect( - ( select ) => select( editSiteStore ).getSettings().showIconLabels, + ( select ) => + !! select( preferencesStore ).get( + 'core/edit-site', + 'showIconLabels' + ), [] ); diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js index b5e5988491396a..87f99e8991e45e 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js @@ -10,7 +10,10 @@ import { parse } from '@wordpress/blocks'; * Internal dependencies */ import { store as editSiteStore } from '../../../store'; -import { PATTERN_CORE_SOURCES, PATTERN_TYPES } from '../../../utils/constants'; +import { + EXCLUDED_PATTERN_SOURCES, + PATTERN_TYPES, +} from '../../../utils/constants'; import { unlock } from '../../../lock-unlock'; function injectThemeAttributeInBlockTemplateContent( @@ -38,9 +41,9 @@ function preparePatterns( patterns, template, currentThemeStylesheet ) { const filterOutDuplicatesByName = ( currentItem, index, items ) => index === items.findIndex( ( item ) => currentItem.name === item.name ); - // Filter out core patterns. - const filterOutCorePatterns = ( pattern ) => - ! PATTERN_CORE_SOURCES.includes( pattern.source ); + // Filter out core/directory patterns not included in theme.json. + const filterOutExcludedPatternSources = ( pattern ) => + ! EXCLUDED_PATTERN_SOURCES.includes( pattern.source ); // Filter only the patterns that are compatible with the current template. const filterCompatiblePatterns = ( pattern ) => @@ -48,9 +51,10 @@ function preparePatterns( patterns, template, currentThemeStylesheet ) { return patterns .filter( - filterOutCorePatterns && - filterOutDuplicatesByName && - filterCompatiblePatterns + ( pattern, index, items ) => + filterOutExcludedPatternSources( pattern ) && + filterOutDuplicatesByName( pattern, index, items ) && + filterCompatiblePatterns( pattern ) ) .map( ( pattern ) => ( { ...pattern, diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js index aa955d5dd33783..45fa8102456a54 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js @@ -63,7 +63,7 @@ function SidebarNavigationScreenGlobalStylesContent() { const { getSettings } = unlock( select( editSiteStore ) ); return { - storedSettings: getSettings( false ), + storedSettings: getSettings(), }; }, [] ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/navigation-menu-editor.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/navigation-menu-editor.js index 7d3be6f631f438..3e19c7f5a29fcb 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/navigation-menu-editor.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/navigation-menu-editor.js @@ -20,7 +20,7 @@ export default function NavigationMenuEditor( { navigationMenuId } ) { const { getSettings } = unlock( select( editSiteStore ) ); return { - storedSettings: getSettings( false ), + storedSettings: getSettings(), }; }, [] ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js index c7862861e9aaf0..7fcedcf40ce134 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/page-details.js @@ -105,26 +105,13 @@ export default function PageDetails( { id } ) { const { record } = useEntityRecord( 'postType', 'page', id ); const { parentTitle, templateTitle, isPostsPage } = useSelect( ( select ) => { - const { getEditedPostContext } = unlock( select( editSiteStore ) ); - const postContext = getEditedPostContext(); - const templates = select( coreStore ).getEntityRecords( + const { getEditedPostId } = unlock( select( editSiteStore ) ); + const template = select( coreStore ).getEntityRecord( 'postType', TEMPLATE_POST_TYPE, - { per_page: -1 } + getEditedPostId() ); - // Template title. - const templateSlug = - // Checks that the post type matches the current theme's post type, otherwise - // the templateSlug returns 'home'. - postContext?.postType === 'page' - ? postContext?.templateSlug - : null; - const _templateTitle = - templates && templateSlug - ? templates.find( - ( template ) => template.slug === templateSlug - )?.title?.rendered - : null; + const _templateTitle = template?.title?.rendered; // Parent page title. const _parentTitle = record?.parent diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js index 9e4983450ae5a3..93cb8d9fbcf42c 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-theme-patterns.js @@ -9,7 +9,7 @@ import { useMemo } from '@wordpress/element'; * Internal dependencies */ import { filterOutDuplicatesByName } from '../page-patterns/utils'; -import { PATTERN_CORE_SOURCES } from '../../utils/constants'; +import { EXCLUDED_PATTERN_SOURCES } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; @@ -32,7 +32,7 @@ export default function useThemePatterns() { [ ...( blockPatterns || [] ), ...( restBlockPatterns || [] ) ] .filter( ( pattern ) => - ! PATTERN_CORE_SOURCES.includes( pattern.source ) + ! EXCLUDED_PATTERN_SOURCES.includes( pattern.source ) ) .filter( filterOutDuplicatesByName ) .filter( ( pattern ) => pattern.inserter !== false ), diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index cbedbabfc3af81..b66bf4390a6bcf 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -59,8 +59,7 @@ function SidebarScreens() { { window?.__experimentalAdminViews && ( } /> diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js index a8a728cfb37edb..9d63001c185c32 100644 --- a/packages/edit-site/src/components/site-hub/index.js +++ b/packages/edit-site/src/components/site-hub/index.js @@ -18,7 +18,7 @@ import { __ } from '@wordpress/i18n'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; -import { forwardRef } from '@wordpress/element'; +import { memo } from '@wordpress/element'; import { search, external } from '@wordpress/icons'; import { store as commandsStore } from '@wordpress/commands'; import { displayShortcut } from '@wordpress/keycodes'; @@ -32,7 +32,7 @@ import { unlock } from '../../lock-unlock'; const HUB_ANIMATION_DURATION = 0.3; -const SiteHub = forwardRef( ( { isTransparent, ...restProps }, ref ) => { +const SiteHub = memo( ( { isTransparent, className } ) => { const { canvasMode, dashboardLink, homeUrl, siteTitle } = useSelect( ( select ) => { const { getCanvasMode, getSettings } = unlock( @@ -84,12 +84,13 @@ const SiteHub = forwardRef( ( { isTransparent, ...restProps }, ref ) => { return ( { - apiFetch( { - path: addQueryArgs( '/wp/v2/templates/lookup', { + return useSelect( + ( select ) => { + const { getEntityRecord, getDefaultTemplateId } = + select( coreStore ); + const templateId = getDefaultTemplateId( { slug, is_custom: isCustom, ignore_empty: true, - } ), - } ).then( ( { content } ) => setTemplateContent( content.raw ) ); - }, [ isCustom, slug ] ); - return templateContent; + } ); + return templateId + ? getEntityRecord( 'postType', TEMPLATE_POST_TYPE, templateId ) + ?.content?.raw + : undefined; + }, + [ slug, isCustom ] + ); } function useStartPatterns( fallbackContent ) { diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js index b178ce501301ef..64eb3778a99c70 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useEffect } from '@wordpress/element'; +import { useEffect, useMemo } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreDataStore } from '@wordpress/core-data'; import { privateApis as routerPrivateApis } from '@wordpress/router'; @@ -20,8 +20,14 @@ import { const { useLocation } = unlock( routerPrivateApis ); -export default function useInitEditedEntityFromURL() { - const { params: { postId, postType } = {} } = useLocation(); +const postTypesWithoutParentTemplate = [ + TEMPLATE_POST_TYPE, + TEMPLATE_PART_POST_TYPE, + NAVIGATION_POST_TYPE, + PATTERN_TYPES.user, +]; + +function useResolveEditedEntityAndContext( { postId, postType } ) { const { isRequestingSite, homepageId, url } = useSelect( ( select ) => { const { getSite, getUnstableBase } = select( coreDataStore ); const siteData = getSite(); @@ -37,58 +43,119 @@ export default function useInitEditedEntityFromURL() { }; }, [] ); - const { - setEditedEntity, - setTemplate, - setTemplatePart, - setPage, - setNavigationMenu, - } = useDispatch( editSiteStore ); + const resolvedTemplateId = useSelect( + ( select ) => { + // If we're rendering a post type that doesn't have a template + // no need to resolve its template. + if ( postTypesWithoutParentTemplate.includes( postType ) ) { + return undefined; + } - useEffect( () => { - if ( postType && postId ) { - switch ( postType ) { - case TEMPLATE_POST_TYPE: - setTemplate( postId ); - break; - case TEMPLATE_PART_POST_TYPE: - setTemplatePart( postId ); - break; - case NAVIGATION_POST_TYPE: - setNavigationMenu( postId ); - break; - case PATTERN_TYPES.user: - setEditedEntity( postType, postId ); - break; - default: - setPage( { - context: { postType, postId }, - } ); + const { + getEditedEntityRecord, + getEntityRecords, + getDefaultTemplateId, + __experimentalGetTemplateForLink, + } = select( coreDataStore ); + + function resolveTemplateForPostTypeAndId( + postTypeToResolve, + postIdToResolve + ) { + const editedEntity = getEditedEntityRecord( + 'postType', + postTypeToResolve, + postIdToResolve + ); + if ( ! editedEntity ) { + return undefined; + } + // First see if the post/page has an assigned template and fetch it. + const currentTemplateSlug = editedEntity.template; + if ( currentTemplateSlug ) { + const currentTemplate = getEntityRecords( + 'postType', + TEMPLATE_POST_TYPE, + { + per_page: -1, + } + )?.find( ( { slug } ) => slug === currentTemplateSlug ); + if ( currentTemplate ) { + return currentTemplate.id; + } + } + + // If no template is assigned, use the default template. + return getDefaultTemplateId( { + slug: `${ postTypeToResolve }-${ editedEntity?.slug }`, + } ); + } + + // If we're rendering a specific page, post... we need to resolve its template. + if ( postType && postId ) { + return resolveTemplateForPostTypeAndId( postType, postId ); + } + + // If we're rendering the home page, and we have a static home page, resolve its template. + if ( homepageId ) { + return resolveTemplateForPostTypeAndId( 'page', homepageId ); + } + + // If we're not rendering a specific page, use the front page template. + if ( ! isRequestingSite && url ) { + const template = __experimentalGetTemplateForLink( url ); + return template?.id; } + }, + [ homepageId, isRequestingSite, url, postId, postType ] + ); + + const context = useMemo( () => { + if ( postTypesWithoutParentTemplate.includes( postType ) ) { + return {}; + } - return; + if ( postType && postId ) { + return { postType, postId }; } - // In all other cases, we need to set the home page in the site editor view. if ( homepageId ) { - setPage( { - context: { postType: 'page', postId: homepageId }, - } ); - } else if ( ! isRequestingSite ) { - setPage( { - path: url, - } ); + return { postType: 'page', postId: homepageId }; } - }, [ - url, - postId, - postType, - homepageId, - isRequestingSite, - setEditedEntity, - setPage, - setTemplate, - setTemplatePart, - setNavigationMenu, - ] ); + + return {}; + }, [ homepageId, postType, postId ] ); + + if ( postTypesWithoutParentTemplate.includes( postType ) ) { + return { isReady: true, postType, postId, context }; + } + + if ( ( postType && postId ) || homepageId || ! isRequestingSite ) { + return { + isReady: resolvedTemplateId !== undefined, + postType: TEMPLATE_POST_TYPE, + postId: resolvedTemplateId, + context, + }; + } + + return { isReady: false }; +} + +export function useInitEditedEntity( params ) { + const { postType, postId, context, isReady } = + useResolveEditedEntityAndContext( params ); + + const { setEditedEntity } = useDispatch( editSiteStore ); + + useEffect( () => { + if ( isReady ) { + setEditedEntity( postType, postId, context ); + } + }, [ isReady, postType, postId, context, setEditedEntity ] ); +} + +export default function useInitEditedEntityFromURL() { + const { params = {} } = useLocation(); + return useInitEditedEntity( params ); } diff --git a/packages/edit-site/src/components/template-part-converter/convert-to-regular.js b/packages/edit-site/src/components/template-part-converter/convert-to-regular.js index b1534ad88a999c..4ca21f42fa944a 100644 --- a/packages/edit-site/src/components/template-part-converter/convert-to-regular.js +++ b/packages/edit-site/src/components/template-part-converter/convert-to-regular.js @@ -26,7 +26,7 @@ export default function ConvertToRegularBlocks( { clientId, onClose } ) { onClose(); } } > - { __( 'Detach blocks from template part' ) } + { __( 'Detach' ) } ); } diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js index f90bf30ce0211d..0407b85c4e8bcd 100644 --- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js +++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js @@ -17,8 +17,9 @@ import { hasBlockSupport, } from '@wordpress/blocks'; import { useContext, useMemo, useCallback } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -391,6 +392,10 @@ function PushChangesToGlobalStylesControl( { const withPushChangesToGlobalStyles = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const blockEditingMode = useBlockEditingMode(); + const isBlockBasedTheme = useSelect( + ( select ) => select( coreStore ).getCurrentTheme()?.is_block_theme, + [] + ); const supportsStyles = SUPPORTED_STYLES.some( ( feature ) => hasBlockSupport( props.name, feature ) ); @@ -398,11 +403,13 @@ const withPushChangesToGlobalStyles = createHigherOrderComponent( return ( <> - { blockEditingMode === 'default' && supportsStyles && ( - - - - ) } + { blockEditingMode === 'default' && + supportsStyles && + isBlockBasedTheme && ( + + + + ) } ); } diff --git a/packages/edit-site/src/hooks/template-part-edit.js b/packages/edit-site/src/hooks/template-part-edit.js index 66f54967c55e22..0b14bbbbd77121 100644 --- a/packages/edit-site/src/hooks/template-part-edit.js +++ b/packages/edit-site/src/hooks/template-part-edit.js @@ -24,11 +24,13 @@ function EditTemplatePartMenuItem( { attributes } ) { const { params } = useLocation(); const templatePart = useSelect( ( select ) => { - return select( coreStore ).getEntityRecord( + const { getCurrentTheme, getEntityRecord } = select( coreStore ); + + return getEntityRecord( 'postType', TEMPLATE_PART_POST_TYPE, // Ideally this should be an official public API. - `${ theme }//${ slug }` + `${ theme || getCurrentTheme()?.stylesheet }//${ slug }` ); }, [ theme, slug ] diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 18cb0d5e5db69b..82014ad06eb493 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -10,10 +10,6 @@ import { import { dispatch } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { createRoot } from '@wordpress/element'; -import { - __experimentalFetchLinkSuggestions as fetchLinkSuggestions, - __experimentalFetchUrlData as fetchUrlData, -} from '@wordpress/core-data'; import { store as editorStore } from '@wordpress/editor'; import { store as interfaceStore } from '@wordpress/interface'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -39,10 +35,6 @@ export function initializeEditor( id, settings ) { const target = document.getElementById( id ); const root = createRoot( target ); - settings.__experimentalFetchLinkSuggestions = ( search, searchOptions ) => - fetchLinkSuggestions( search, searchOptions, settings ); - settings.__experimentalFetchRichUrlData = fetchUrlData; - dispatch( blocksStore ).reapplyBlockTypeFilters(); const coreBlocks = __experimentalGetCoreBlocks().filter( ( { name } ) => name !== 'core/freeform' diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index ce698a757f6bb3..4de1f4ac2a61fd 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -31,11 +31,14 @@ import { */ export function toggleFeature( featureName ) { return function ( { registry } ) { - deprecated( "select( 'core/edit-site' ).toggleFeature( featureName )", { - since: '6.0', - alternative: - "select( 'core/preferences').toggle( 'core/edit-site', featureName )", - } ); + deprecated( + "dispatch( 'core/edit-site' ).toggleFeature( featureName )", + { + since: '6.0', + alternative: + "dispatch( 'core/preferences').toggle( 'core/edit-site', featureName )", + } + ); registry .dispatch( preferencesStore ) @@ -60,44 +63,38 @@ export function __experimentalSetPreviewDeviceType( deviceType ) { /** * Action that sets a template, optionally fetching it from REST API. * - * @param {number} templateId The template ID. - * @param {string} templateSlug The template slug. * @return {Object} Action object. */ -export const setTemplate = - ( templateId, templateSlug ) => - async ( { dispatch, registry } ) => { - if ( ! templateSlug ) { - try { - const template = await registry - .resolveSelect( coreStore ) - .getEntityRecord( - 'postType', - TEMPLATE_POST_TYPE, - templateId - ); - templateSlug = template?.slug; - } catch ( error ) {} - } +export function setTemplate() { + deprecated( "dispatch( 'core/edit-site' ).setTemplate", { + since: '6.5', + version: '6.8', + hint: 'The setTemplate is not needed anymore, the correct entity is resolved from the URL automatically.', + } ); - dispatch( { - type: 'SET_EDITED_POST', - postType: TEMPLATE_POST_TYPE, - id: templateId, - context: { templateSlug }, - } ); + return { + type: 'NOTHING', }; +} /** * Action that adds a new template and sets it as the current template. * * @param {Object} template The template. * + * @deprecated + * * @return {Object} Action object used to set the current template. */ export const addTemplate = ( template ) => async ( { dispatch, registry } ) => { + deprecated( "dispatch( 'core/edit-site' ).addTemplate", { + since: '6.5', + version: '6.8', + hint: 'use saveEntityRecord directly', + } ); + const newTemplate = await registry .dispatch( coreStore ) .saveEntityRecord( 'postType', TEMPLATE_POST_TYPE, template ); @@ -118,7 +115,6 @@ export const addTemplate = type: 'SET_EDITED_POST', postType: TEMPLATE_POST_TYPE, id: newTemplate.id, - context: { templateSlug: newTemplate.slug }, } ); }; @@ -211,14 +207,16 @@ export function setNavigationMenu( navigationMenuId ) { * * @param {string} postType The entity's post type. * @param {string} postId The entity's ID. + * @param {Object} context The entity's context. * * @return {Object} Action object. */ -export function setEditedEntity( postType, postId ) { +export function setEditedEntity( postType, postId, context ) { return { type: 'SET_EDITED_POST', postType, id: postId, + context, }; } @@ -254,75 +252,19 @@ export function setEditedPostContext( context ) { * Resolves the template for a page and displays both. If no path is given, attempts * to use the postId to generate a path like `?p=${ postId }`. * - * @param {Object} page The page object. - * @param {string} page.type The page type. - * @param {string} page.slug The page slug. - * @param {string} page.path The page path. - * @param {Object} page.context The page context. + * @deprecated * * @return {number} The resolved template ID for the page route. */ -export const setPage = - ( page ) => - async ( { dispatch, registry } ) => { - let template; - const getDefaultTemplate = async ( slug ) => - apiFetch( { - path: addQueryArgs( '/wp/v2/templates/lookup', { - slug: `page-${ slug }`, - } ), - } ); - - if ( page.path ) { - template = await registry - .resolveSelect( coreStore ) - .__experimentalGetTemplateForLink( page.path ); - } else { - const editedEntity = await registry - .resolveSelect( coreStore ) - .getEditedEntityRecord( - 'postType', - page.context?.postType || 'post', - page.context?.postId - ); - const currentTemplateSlug = editedEntity?.template; - if ( currentTemplateSlug ) { - const currentTemplate = ( - await registry - .resolveSelect( coreStore ) - .getEntityRecords( 'postType', TEMPLATE_POST_TYPE, { - per_page: -1, - } ) - )?.find( ( { slug } ) => slug === currentTemplateSlug ); - if ( currentTemplate ) { - template = currentTemplate; - } else { - // If a page has a `template` set and is not included in the list - // of the current theme's templates, query for current theme's default template. - template = await getDefaultTemplate( editedEntity?.slug ); - } - } else { - // Page's `template` is empty, that indicates we need to use the default template for the page. - template = await getDefaultTemplate( editedEntity?.slug ); - } - } - - if ( ! template ) { - return; - } - - dispatch( { - type: 'SET_EDITED_POST', - postType: TEMPLATE_POST_TYPE, - id: template.id, - context: { - ...page.context, - templateSlug: template.slug, - }, - } ); +export function setPage() { + deprecated( "dispatch( 'core/edit-site' ).setPage", { + since: '6.5', + version: '6.8', + hint: 'The setPage is not needed anymore, the correct entity is resolved from the URL automatically.', + } ); - return template.id; - }; + return { type: 'NOTHING' }; +} /** * Action that sets the active navigation panel menu. diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index 9e861c7567e4ac..f9c2f7d65cfaf4 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -1,15 +1,9 @@ -/** - * External dependencies - */ -import createSelector from 'rememo'; - /** * WordPress dependencies */ import { store as coreDataStore } from '@wordpress/core-data'; import { createRegistrySelector } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; -import { uploadMedia } from '@wordpress/media-utils'; import { Platform } from '@wordpress/element'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as blockEditorStore } from '@wordpress/block-editor'; @@ -18,28 +12,11 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; * Internal dependencies */ import { getFilteredTemplatePartBlocks } from './utils'; -import { - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, -} from '../utils/constants'; +import { TEMPLATE_PART_POST_TYPE } from '../utils/constants'; /** * @typedef {'template'|'template_type'} TemplateType Template type. */ -/** - * Helper for getting a preference from the preferences store. - * - * This is only present so that `getSettings` doesn't need to be made a - * registry selector. - * - * It's unstable because the selector needs to be exported and so part of the - * public API to work. - */ -export const __unstableGetPreference = createRegistrySelector( - ( select ) => ( state, name ) => - select( preferencesStore ).get( 'core/edit-site', name ) -); - /** * Returns whether the given feature is enabled or not. * @@ -49,14 +26,19 @@ export const __unstableGetPreference = createRegistrySelector( * * @return {boolean} Is active. */ -export function isFeatureActive( state, featureName ) { - deprecated( `select( 'core/edit-site' ).isFeatureActive`, { - since: '6.0', - alternative: `select( 'core/preferences' ).get`, - } ); +export const isFeatureActive = createRegistrySelector( + ( select ) => ( _, featureName ) => { + deprecated( `select( 'core/edit-site' ).isFeatureActive`, { + since: '6.0', + alternative: `select( 'core/preferences' ).get`, + } ); - return !! __unstableGetPreference( state, featureName ); -} + return !! select( preferencesStore ).get( + 'core/edit-site', + featureName + ); + } +); /** * Returns the current editing canvas device type. @@ -88,6 +70,13 @@ export const getCanUserCreateMedia = createRegistrySelector( * @return {Array} The available reusable blocks. */ export const getReusableBlocks = createRegistrySelector( ( select ) => () => { + deprecated( + "select( 'core/core' ).getEntityRecords( 'postType', 'wp_block' )", + { + since: '6.5', + version: '6.8', + } + ); const isWeb = Platform.OS === 'web'; return isWeb ? select( coreDataStore ).getEntityRecords( 'postType', 'wp_block', { @@ -97,67 +86,18 @@ export const getReusableBlocks = createRegistrySelector( ( select ) => () => { } ); /** - * Returns the settings, taking into account active features and permissions. + * Returns the site editor settings. * - * @param {Object} state Global application state. - * @param {Function} setIsInserterOpen Setter for the open state of the global inserter. + * @param {Object} state Global application state. * * @return {Object} Settings. */ -export const getSettings = createSelector( - ( state, setIsInserterOpen ) => { - const settings = { - ...state.settings, - outlineMode: true, - focusMode: !! __unstableGetPreference( state, 'focusMode' ), - isDistractionFree: !! __unstableGetPreference( - state, - 'distractionFree' - ), - hasFixedToolbar: !! __unstableGetPreference( - state, - 'fixedToolbar' - ), - keepCaretInsideBlock: !! __unstableGetPreference( - state, - 'keepCaretInsideBlock' - ), - showIconLabels: !! __unstableGetPreference( - state, - 'showIconLabels' - ), - __experimentalSetIsInserterOpened: setIsInserterOpen, - __experimentalReusableBlocks: getReusableBlocks( state ), - __experimentalPreferPatternsOnRoot: - TEMPLATE_POST_TYPE === getEditedPostType( state ), - }; - - const canUserCreateMedia = getCanUserCreateMedia( state ); - if ( ! canUserCreateMedia ) { - return settings; - } - - settings.mediaUpload = ( { onError, ...rest } ) => { - uploadMedia( { - wpAllowedMimeTypes: state.settings.allowedMimeTypes, - onError: ( { message } ) => onError( message ), - ...rest, - } ); - }; - return settings; - }, - ( state ) => [ - getCanUserCreateMedia( state ), - state.settings, - __unstableGetPreference( state, 'focusMode' ), - __unstableGetPreference( state, 'distractionFree' ), - __unstableGetPreference( state, 'fixedToolbar' ), - __unstableGetPreference( state, 'keepCaretInsideBlock' ), - __unstableGetPreference( state, 'showIconLabels' ), - getReusableBlocks( state ), - getEditedPostType( state ), - ] -); +export function getSettings( state ) { + // It is important that we don't inject anything into these settings locally. + // The reason for this is that we have an effect in place that calls setSettings based on the previous value of getSettings. + // If we add computed settings here, we'll be adding these computed settings to the state which is very unexpected. + return state.settings; +} /** * @deprecated @@ -317,9 +257,9 @@ export const getCurrentTemplateTemplateParts = createRegistrySelector( * * @return {string} Editing mode. */ -export function getEditorMode( state ) { - return __unstableGetPreference( state, 'editorMode' ); -} +export const getEditorMode = createRegistrySelector( ( select ) => () => { + return select( preferencesStore ).get( 'core/edit-site', 'editorMode' ); +} ); /** * @deprecated diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index 5fbbf62f369e87..787809acda0890 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import apiFetch from '@wordpress/api-fetch'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { createRegistry } from '@wordpress/data'; @@ -15,18 +14,6 @@ import { store as preferencesStore } from '@wordpress/preferences'; import { store as editSiteStore } from '..'; import { setHasPageContentFocus } from '../actions'; -const ENTITY_TYPES = { - wp_template: { - description: 'Templates to include in your theme.', - hierarchical: false, - name: 'Templates', - rest_base: 'templates', - rest_namespace: 'wp/v2', - slug: 'wp_template', - taxonomies: [], - }, -}; - function createRegistryWithStores() { // create a registry const registry = createRegistry(); @@ -47,21 +34,27 @@ describe( 'actions', () => { it( 'should toggle a feature flag', () => { const registry = createRegistryWithStores(); - // Should default to false. + // Should start as undefined. expect( - registry.select( editSiteStore ).isFeatureActive( 'name' ) - ).toBe( false ); + registry + .select( preferencesStore ) + .get( 'core/edit-site', 'name' ) + ).toBe( undefined ); // Toggle on. registry.dispatch( editSiteStore ).toggleFeature( 'name' ); expect( - registry.select( editSiteStore ).isFeatureActive( 'name' ) + registry + .select( preferencesStore ) + .get( 'core/edit-site', 'name' ) ).toBe( true ); // Toggle off again. registry.dispatch( editSiteStore ).toggleFeature( 'name' ); expect( - registry.select( editSiteStore ).isFeatureActive( 'name' ) + registry + .select( preferencesStore ) + .get( 'core/edit-site', 'name' ) ).toBe( false ); // Expect a deprecation warning. @@ -69,86 +62,6 @@ describe( 'actions', () => { } ); } ); - describe( 'setTemplate', () => { - const ID = 1; - const SLUG = 'archive'; - - it( 'should set the template when slug is provided', async () => { - const registry = createRegistryWithStores(); - - await registry.dispatch( editSiteStore ).setTemplate( ID, SLUG ); - - const select = registry.select( editSiteStore ); - expect( select.getEditedPostId() ).toBe( ID ); - expect( select.getEditedPostContext().templateSlug ).toBe( SLUG ); - } ); - - it( 'should set the template by fetching the template slug', async () => { - const registry = createRegistryWithStores(); - - apiFetch.setFetchHandler( async ( options ) => { - const { method = 'GET', path } = options; - if ( method === 'GET' ) { - if ( path.startsWith( '/wp/v2/types' ) ) { - return ENTITY_TYPES; - } - - if ( path.startsWith( `/wp/v2/templates/${ ID }` ) ) { - return { id: ID, slug: SLUG }; - } - } - - throw { - code: 'unknown_path', - message: `Unknown path: ${ method } ${ path }`, - }; - } ); - - await registry.dispatch( editSiteStore ).setTemplate( ID ); - - const select = registry.select( editSiteStore ); - expect( select.getEditedPostId() ).toBe( ID ); - expect( select.getEditedPostContext().templateSlug ).toBe( SLUG ); - } ); - } ); - - describe( 'addTemplate', () => { - it( 'should issue a REST request to create the template and then set it', async () => { - const registry = createRegistryWithStores(); - - const ID = 1; - const SLUG = 'index'; - - apiFetch.setFetchHandler( async ( options ) => { - const { method = 'GET', path, data } = options; - - if ( method === 'GET' && path.startsWith( '/wp/v2/types' ) ) { - return ENTITY_TYPES; - } - - if ( - method === 'POST' && - path.startsWith( '/wp/v2/templates' ) - ) { - return { id: ID, slug: data.slug }; - } - - throw { - code: 'unknown_path', - message: `Unknown path: ${ method } ${ path }`, - }; - } ); - - await registry - .dispatch( editSiteStore ) - .addTemplate( { slug: SLUG } ); - - const select = registry.select( editSiteStore ); - expect( select.getEditedPostId() ).toBe( ID ); - expect( select.getEditedPostContext().templateSlug ).toBe( SLUG ); - } ); - } ); - describe( 'setTemplatePart', () => { it( 'should set template part', () => { const registry = createRegistryWithStores(); @@ -162,45 +75,6 @@ describe( 'actions', () => { } ); } ); - describe( 'setPage', () => { - it( 'should find the template and then set the page', async () => { - const registry = createRegistryWithStores(); - - const ID = 'emptytheme//single'; - const SLUG = 'single'; - - apiFetch.setFetchHandler( async ( options ) => { - const { method = 'GET', path, url } = options; - - // Called with url arg in `__experimentalGetTemplateForLink` - if ( url ) { - return { data: { id: ID, slug: SLUG } }; - } - - if ( method === 'GET' ) { - if ( path.startsWith( '/wp/v2/types' ) ) { - return ENTITY_TYPES; - } - - if ( path.startsWith( `/wp/v2/templates/${ ID }` ) ) { - return { id: ID, slug: SLUG }; - } - } - - throw { - code: 'unknown_path', - message: `Unknown path: ${ method } ${ path }`, - }; - } ); - - await registry.dispatch( editSiteStore ).setPage( { path: '/' } ); - - const select = registry.select( editSiteStore ); - expect( select.getEditedPostId() ).toBe( 'emptytheme//single' ); - expect( select.getEditedPostType() ).toBe( 'wp_template' ); - } ); - } ); - describe( 'setIsListViewOpened', () => { it( 'should set the list view opened state', () => { const registry = createRegistryWithStores(); diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index 9380f8ea4d276c..7e36d2f4b75f4d 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -8,30 +8,19 @@ import { store as coreDataStore } from '@wordpress/core-data'; */ import { getCanUserCreateMedia, - getSettings, getEditedPostType, getEditedPostId, - getReusableBlocks, isInserterOpened, isListViewOpened, - __unstableGetPreference, isPage, hasPageContentFocus, } from '../selectors'; describe( 'selectors', () => { const canUser = jest.fn( () => true ); - const getEntityRecords = jest.fn( () => [] ); - const get = jest.fn(); getCanUserCreateMedia.registry = { select: jest.fn( () => ( { canUser } ) ), }; - getReusableBlocks.registry = { - select: jest.fn( () => ( { getEntityRecords } ) ), - }; - __unstableGetPreference.registry = { - select: jest.fn( () => ( { get } ) ), - }; describe( 'getCanUserCreateMedia', () => { it( "selects `canUser( 'create', 'media' )` from the core store", () => { @@ -43,77 +32,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'getReusableBlocks', () => { - it( "selects `getEntityRecords( 'postType', 'wp_block' )` from the core store", () => { - expect( getReusableBlocks() ).toEqual( [] ); - expect( getReusableBlocks.registry.select ).toHaveBeenCalledWith( - coreDataStore - ); - expect( getEntityRecords ).toHaveBeenCalledWith( - 'postType', - 'wp_block', - { - per_page: -1, - } - ); - } ); - } ); - - describe( 'getSettings', () => { - it( "returns the settings when the user can't create media", () => { - canUser.mockReturnValueOnce( false ); - canUser.mockReturnValueOnce( false ); - get.mockImplementation( ( scope, name ) => { - if ( name === 'focusMode' ) return false; - if ( name === 'fixedToolbar' ) return false; - } ); - const state = { - settings: {}, - preferences: {}, - editedPost: { postType: 'wp_template' }, - }; - const setInserterOpened = () => {}; - expect( getSettings( state, setInserterOpened ) ).toEqual( { - outlineMode: true, - focusMode: false, - hasFixedToolbar: false, - isDistractionFree: false, - keepCaretInsideBlock: false, - showIconLabels: false, - __experimentalSetIsInserterOpened: setInserterOpened, - __experimentalReusableBlocks: [], - __experimentalPreferPatternsOnRoot: true, - } ); - } ); - - it( 'returns the extended settings when the user can create media', () => { - get.mockImplementation( ( scope, name ) => { - if ( name === 'focusMode' ) return true; - if ( name === 'fixedToolbar' ) return true; - } ); - - const state = { - settings: { key: 'value' }, - editedPost: { postType: 'wp_template_part' }, - }; - const setInserterOpened = () => {}; - - expect( getSettings( state, setInserterOpened ) ).toEqual( { - outlineMode: true, - key: 'value', - focusMode: true, - hasFixedToolbar: true, - isDistractionFree: false, - keepCaretInsideBlock: false, - showIconLabels: false, - __experimentalSetIsInserterOpened: setInserterOpened, - __experimentalReusableBlocks: [], - mediaUpload: expect.any( Function ), - __experimentalPreferPatternsOnRoot: false, - } ); - } ); - } ); - describe( 'getEditedPostId', () => { it( 'returns the template ID', () => { const state = { editedPost: { id: 10 } }; diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index dadbf48d06e64d..0b49f48a3e5845 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -39,6 +39,7 @@ @import "./components/sidebar-navigation-screen-pattern/style.scss"; @import "./components/sidebar-navigation-screen-patterns/style.scss"; @import "./components/sidebar-navigation-screen-template/style.scss"; +@import "./components/sidebar-dataviews/style.scss"; @import "./components/site-hub/style.scss"; @import "./components/sidebar-navigation-screen-navigation-menus/style.scss"; @import "./components/site-icon/style.scss"; diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js index 7d123818043801..2f00bc13f6de8d 100644 --- a/packages/edit-site/src/utils/constants.js +++ b/packages/edit-site/src/utils/constants.js @@ -27,7 +27,7 @@ export const { PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, PATTERN_USER_CATEGORY, - PATTERN_CORE_SOURCES, + EXCLUDED_PATTERN_SOURCES, PATTERN_SYNC_TYPES, } = unlock( patternPrivateApis ); diff --git a/packages/edit-widgets/src/components/header/document-tools/index.js b/packages/edit-widgets/src/components/header/document-tools/index.js new file mode 100644 index 00000000000000..8cac7590be5e88 --- /dev/null +++ b/packages/edit-widgets/src/components/header/document-tools/index.js @@ -0,0 +1,130 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { __, _x } from '@wordpress/i18n'; +import { Button, ToolbarItem } from '@wordpress/components'; +import { + NavigableToolbar, + store as blockEditorStore, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { listView, plus } from '@wordpress/icons'; +import { useCallback, useRef } from '@wordpress/element'; +import { useViewportMatch } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import UndoButton from '../undo-redo/undo'; +import RedoButton from '../undo-redo/redo'; +import useLastSelectedWidgetArea from '../../../hooks/use-last-selected-widget-area'; +import { store as editWidgetsStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; + +const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); + +function DocumentTools( { setListViewToggleElement } ) { + const isMediumViewport = useViewportMatch( 'medium' ); + const inserterButton = useRef(); + const widgetAreaClientId = useLastSelectedWidgetArea(); + const isLastSelectedWidgetAreaOpen = useSelect( + ( select ) => + select( editWidgetsStore ).getIsWidgetAreaOpen( + widgetAreaClientId + ), + [ widgetAreaClientId ] + ); + const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => { + const { isInserterOpened, isListViewOpened } = + select( editWidgetsStore ); + return { + isInserterOpen: isInserterOpened(), + isListViewOpen: isListViewOpened(), + }; + }, [] ); + const { setIsWidgetAreaOpen, setIsInserterOpened, setIsListViewOpened } = + useDispatch( editWidgetsStore ); + const { selectBlock } = useDispatch( blockEditorStore ); + const handleClick = () => { + if ( isInserterOpen ) { + // Focusing the inserter button closes the inserter popover. + setIsInserterOpened( false ); + } else { + if ( ! isLastSelectedWidgetAreaOpen ) { + // Select the last selected block if hasn't already. + selectBlock( widgetAreaClientId ); + // Open the last selected widget area when opening the inserter. + setIsWidgetAreaOpen( widgetAreaClientId, true ); + } + // The DOM updates resulting from selectBlock() and setIsInserterOpened() calls are applied the + // same tick and pretty much in a random order. The inserter is closed if any other part of the + // app receives focus. If selectBlock() happens to take effect after setIsInserterOpened() then + // the inserter is visible for a brief moment and then gets auto-closed due to focus moving to + // the selected block. + window.requestAnimationFrame( () => setIsInserterOpened( true ) ); + } + }; + + const toggleListView = useCallback( + () => setIsListViewOpened( ! isListViewOpen ), + [ setIsListViewOpened, isListViewOpen ] + ); + + const { + shouldShowContextualToolbar, + canFocusHiddenToolbar, + fixedToolbarCanBeFocused, + } = useShouldContextualToolbarShow(); + // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. + // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. + const blockToolbarCanBeFocused = + shouldShowContextualToolbar || + canFocusHiddenToolbar || + fixedToolbarCanBeFocused; + + return ( + + { + event.preventDefault(); + } } + onClick={ handleClick } + icon={ plus } + /* translators: button label text should, if possible, be under 16 + characters. */ + label={ _x( + 'Toggle block inserter', + 'Generic label for block inserter button' + ) } + /> + { isMediumViewport && ( + <> + + + + + ) } + + ); +} + +export default DocumentTools; diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js index 0308c2c2171e24..9251f528ca5ee4 100644 --- a/packages/edit-widgets/src/components/header/index.js +++ b/packages/edit-widgets/src/components/header/index.js @@ -1,101 +1,48 @@ /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { __, _x } from '@wordpress/i18n'; -import { Button, ToolbarItem, VisuallyHidden } from '@wordpress/components'; -import { - NavigableToolbar, - store as blockEditorStore, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { useRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { Popover, VisuallyHidden } from '@wordpress/components'; import { PinnedItems } from '@wordpress/interface'; -import { listView, plus } from '@wordpress/icons'; -import { useCallback, useRef } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies */ +import DocumentTools from './document-tools'; import SaveButton from '../save-button'; -import UndoButton from './undo-redo/undo'; -import RedoButton from './undo-redo/redo'; import MoreMenu from '../more-menu'; -import useLastSelectedWidgetArea from '../../hooks/use-last-selected-widget-area'; -import { store as editWidgetsStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); +const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); function Header( { setListViewToggleElement } ) { - const isMediumViewport = useViewportMatch( 'medium' ); - const inserterButton = useRef(); - const widgetAreaClientId = useLastSelectedWidgetArea(); - const isLastSelectedWidgetAreaOpen = useSelect( - ( select ) => - select( editWidgetsStore ).getIsWidgetAreaOpen( - widgetAreaClientId + const isLargeViewport = useViewportMatch( 'medium' ); + const blockToolbarRef = useRef(); + const { hasFixedToolbar } = useSelect( + ( select ) => ( { + hasFixedToolbar: !! select( preferencesStore ).get( + 'core/edit-widgets', + 'fixedToolbar' ), - [ widgetAreaClientId ] + } ), + [] ); - const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => { - const { isInserterOpened, isListViewOpened } = - select( editWidgetsStore ); - return { - isInserterOpen: isInserterOpened(), - isListViewOpen: isListViewOpened(), - }; - }, [] ); - const { setIsWidgetAreaOpen, setIsInserterOpened, setIsListViewOpened } = - useDispatch( editWidgetsStore ); - const { selectBlock } = useDispatch( blockEditorStore ); - const handleClick = () => { - if ( isInserterOpen ) { - // Focusing the inserter button closes the inserter popover. - setIsInserterOpened( false ); - } else { - if ( ! isLastSelectedWidgetAreaOpen ) { - // Select the last selected block if hasn't already. - selectBlock( widgetAreaClientId ); - // Open the last selected widget area when opening the inserter. - setIsWidgetAreaOpen( widgetAreaClientId, true ); - } - // The DOM updates resulting from selectBlock() and setIsInserterOpened() calls are applied the - // same tick and pretty much in a random order. The inserter is closed if any other part of the - // app receives focus. If selectBlock() happens to take effect after setIsInserterOpened() then - // the inserter is visible for a brief moment and then gets auto-closed due to focus moving to - // the selected block. - window.requestAnimationFrame( () => setIsInserterOpened( true ) ); - } - }; - - const toggleListView = useCallback( - () => setIsListViewOpened( ! isListViewOpen ), - [ setIsListViewOpened, isListViewOpen ] - ); - - const { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - } = useShouldContextualToolbarShow(); - // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. - // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. - const blockToolbarCanBeFocused = - shouldShowContextualToolbar || - canFocusHiddenToolbar || - fixedToolbarCanBeFocused; return ( <>
    - { isMediumViewport && ( + { isLargeViewport && (

    { __( 'Widgets' ) }

    ) } - { ! isMediumViewport && ( + { ! isLargeViewport && ( ) } - - { - event.preventDefault(); - } } - onClick={ handleClick } - icon={ plus } - /* translators: button label text should, if possible, be under 16 - characters. */ - label={ _x( - 'Toggle block inserter', - 'Generic label for block inserter button' - ) } - /> - { isMediumViewport && ( - <> - - - - - ) } - + + { hasFixedToolbar && isLargeViewport && ( + <> +
    + +
    + + + ) }
    diff --git a/packages/edit-widgets/src/components/header/style.scss b/packages/edit-widgets/src/components/header/style.scss index 64a9f124bb7502..e279b0f79b4585 100644 --- a/packages/edit-widgets/src/components/header/style.scss +++ b/packages/edit-widgets/src/components/header/style.scss @@ -9,12 +9,22 @@ @include break-small { overflow: visible; } + + .selected-block-tools-wrapper { + overflow-x: hidden; + } + + .block-editor-block-contextual-toolbar.is-fixed { + border: none; + } } .edit-widgets-header__navigable-toolbar-wrapper { display: flex; align-items: center; justify-content: center; + flex-shrink: 2; + overflow-x: hidden; padding-left: $grid-unit-20; } diff --git a/packages/edit-widgets/src/components/layout/style.scss b/packages/edit-widgets/src/components/layout/style.scss index fe1edbae232951..1aed3d3eefc86f 100644 --- a/packages/edit-widgets/src/components/layout/style.scss +++ b/packages/edit-widgets/src/components/layout/style.scss @@ -22,15 +22,3 @@ height: 100%; } } - -.blocks-widgets-container { - // making the header be lower than the content - // so the fixed toolbar can be positioned on top of it - // but only on desktop - @include break-medium() { - .interface-interface-skeleton__header:not(:focus-within) { - z-index: 19; - } - } - -} diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss b/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss index 062214ef147bf1..35493cad130cfd 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss @@ -34,101 +34,3 @@ } } } - -// Fixed contextual toolbar -@include editor-left(".edit-widgets-block-editor .block-editor-block-contextual-toolbar.is-fixed"); - - -.edit-widgets-block-editor .block-editor-block-contextual-toolbar.is-fixed { - position: sticky; - top: 0; - z-index: z-index(".block-editor-block-popover"); - display: block; - width: 100%; - - // on desktop and tablet viewports the toolbar is fixed - // on top of interface header - $toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05; - - @include break-medium() { - // leave room for block inserter, undo and redo, list view - margin-left: $toolbar-margin; - // position on top of interface header - position: fixed; - top: $admin-bar-height; - // Don't fill up when empty - min-height: initial; - // remove the border - border-bottom: none; - // has to be flex for collapse button to fit - display: flex; - - // Mimic the height of the parent, vertically align center, and provide a max-height. - height: $header-height; - align-items: center; - - - // on tablet viewports the toolbar is fixed - // on top of interface header and covers the whole header - // except for the inserter on the left - width: calc(100% - #{$toolbar-margin}); - - &.is-collapsed { - width: initial; - } - - &:empty { - width: initial; - } - - .is-fullscreen-mode & { - // leave room for block inserter, undo and redo, list view - // and some margin left - margin-left: $grid-unit-80 * 4 - 2 * $grid-unit; - - top: 0; - - &.is-collapsed { - width: initial; - } - - &:empty { - width: initial; - } - } - - .show-icon-labels & { - margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin - width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons - - .is-fullscreen-mode & { - margin-left: $grid-unit * 18; // site hub, inserter and margin - } - } - - .blocks-widgets-container & { - margin-left: $grid-unit-80 * 2.4; - - &.is-collapsed { - margin-left: $grid-unit-80 * 4.2; - } - } - } - - // on desktop viewports the toolbar is fixed - // on top of interface header and leaves room - // for the block inserter the publish button - @include break-large() { - width: auto; - .show-icon-labels & { - width: auto; //there are no undo, redo and list view buttons - } - - .is-fullscreen-mode & { - // in full screen mode we need to account for - // the combined with of the tools at the right of the header and the margin left - // of the toolbar which includes four buttons - width: calc(100% - 280px - #{4 * $grid-unit-80}); - } - } -} diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 39b562806c109a..bcfccc026ff727 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -71,6 +71,7 @@ export { default as PostTypeSupportCheck } from './post-type-support-check'; export { default as PostURL } from './post-url'; export { default as PostURLCheck } from './post-url/check'; export { default as PostURLLabel, usePostURLLabel } from './post-url/label'; +export { default as PostURLPanel } from './post-url/panel'; export { default as PostVisibility } from './post-visibility'; export { default as PostVisibilityLabel, diff --git a/packages/editor/src/components/page-attributes/order.js b/packages/editor/src/components/page-attributes/order.js index 416636d14bdbf8..4a751c0b151aba 100644 --- a/packages/editor/src/components/page-attributes/order.js +++ b/packages/editor/src/components/page-attributes/order.js @@ -39,6 +39,7 @@ function PageAttributesOrder() { span { display: block; - width: 45%; - flex-shrink: 0; - padding: $grid-unit-15 * 0.5 0; + width: 30%; + margin-right: 8px; word-break: break-word; } diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index d7094b080de9d3..09f5f30c2a660c 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -222,7 +222,6 @@ function PostTitle( _, forwardedRef ) { } ); }, __unstableDisableFormats: true, - preserveWhiteSpace: true, } ); /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js index 0b58ee774b699e..1ec0dd3ade3bfc 100644 --- a/packages/editor/src/components/post-title/index.native.js +++ b/packages/editor/src/components/post-title/index.native.js @@ -7,18 +7,14 @@ import { View } from 'react-native'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { - __experimentalRichText as RichText, - create, - insert, -} from '@wordpress/rich-text'; +import { create, insert } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { withDispatch, withSelect } from '@wordpress/data'; import { withFocusOutside } from '@wordpress/components'; import { withInstanceId, compose } from '@wordpress/compose'; import { __, sprintf } from '@wordpress/i18n'; import { pasteHandler } from '@wordpress/blocks'; -import { store as blockEditorStore } from '@wordpress/block-editor'; +import { store as blockEditorStore, RichText } from '@wordpress/block-editor'; import { store as editorStore } from '@wordpress/editor'; /** @@ -152,7 +148,7 @@ class PostTitle extends Component { accessibilityLabel={ this.getTitle( title, postType ) } accessibilityHint={ __( 'Updates the title.' ) } > - {} } - > + /> ); } diff --git a/packages/edit-post/src/components/sidebar/post-url/index.js b/packages/editor/src/components/post-url/panel.js similarity index 68% rename from packages/edit-post/src/components/sidebar/post-url/index.js rename to packages/editor/src/components/post-url/panel.js index 1dc1b8d804cd77..1fddc7df9922c4 100644 --- a/packages/edit-post/src/components/sidebar/post-url/index.js +++ b/packages/editor/src/components/post-url/panel.js @@ -2,15 +2,21 @@ * WordPress dependencies */ import { useMemo, useState } from '@wordpress/element'; -import { PanelRow, Dropdown, Button } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; import { - PostURLCheck, - PostURL as PostURLForm, - usePostURLLabel, -} from '@wordpress/editor'; + __experimentalHStack as HStack, + Dropdown, + Button, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import PostURLCheck from './check'; +import PostURL from './index'; +import { usePostURLLabel } from './label'; -export default function PostURL() { +export default function PostURLPanel() { // Use internal state instead of a ref to make sure that the component // re-renders when the popover's anchor updates. const [ popoverAnchor, setPopoverAnchor ] = useState( null ); @@ -22,21 +28,21 @@ export default function PostURL() { return ( - + { __( 'URL' ) } ( ) } renderContent={ ( { onClose } ) => ( - + ) } /> - + ); } @@ -45,7 +51,7 @@ function PostURLToggle( { isOpen, onClick } ) { const label = usePostURLLabel(); return (
    \n", - "originalContent": "\n
    \n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\n
    \n" + "originalContent": "\n
    \n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n\n
    \n" }, "innerBlocks": [ { @@ -14,7 +14,7 @@ "attributes": { "originalName": "core/form-input", "originalUndelimitedContent": "", - "originalContent": "\n\n" + "originalContent": "\n\n" }, "innerBlocks": [] }, @@ -24,7 +24,7 @@ "attributes": { "originalName": "core/form-input", "originalUndelimitedContent": "", - "originalContent": "\n\n" + "originalContent": "\n\n" }, "innerBlocks": [] }, @@ -34,7 +34,7 @@ "attributes": { "originalName": "core/form-input", "originalUndelimitedContent": "", - "originalContent": "\n\n" + "originalContent": "\n\n" }, "innerBlocks": [] }, @@ -44,7 +44,7 @@ "attributes": { "originalName": "core/form-input", "originalUndelimitedContent": "", - "originalContent": "\n\n" + "originalContent": "\n\n" }, "innerBlocks": [] }, diff --git a/test/integration/fixtures/blocks/core__form.serialized.html b/test/integration/fixtures/blocks/core__form.serialized.html index 58a2a49967eb56..585a50868b85e0 100644 --- a/test/integration/fixtures/blocks/core__form.serialized.html +++ b/test/integration/fixtures/blocks/core__form.serialized.html @@ -1,16 +1,16 @@
    - + - + - + - +
    diff --git a/test/native/setup.js b/test/native/setup.js index 00fb95070d84d7..53ab28f861a1ef 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -151,6 +151,7 @@ jest.mock( 'react-native-svg', () => { G: () => 'G', Polygon: () => 'Polygon', Rect: () => 'Rect', + SvgXml: jest.fn(), }; } ); diff --git a/test/performance/config/performance-reporter.ts b/test/performance/config/performance-reporter.ts index decea4cb7a9a46..fa7cc90825c220 100644 --- a/test/performance/config/performance-reporter.ts +++ b/test/performance/config/performance-reporter.ts @@ -32,6 +32,7 @@ export interface WPRawPerformanceResults { inserterSearch: number[]; inserterHover: number[]; listViewOpen: number[]; + navigate: number[]; } export interface WPPerformanceResults { @@ -65,6 +66,7 @@ export interface WPPerformanceResults { listViewOpen?: number; minListViewOpen?: number; maxListViewOpen?: number; + navigate?: number; } /** @@ -108,6 +110,7 @@ export function curateResults( listViewOpen: average( results.listViewOpen ), minListViewOpen: minimum( results.listViewOpen ), maxListViewOpen: maximum( results.listViewOpen ), + navigate: median( results.navigate ), }; return ( diff --git a/test/performance/fixtures/perf-utils.ts b/test/performance/fixtures/perf-utils.ts index dcd9579364e10b..d17eec2c935b1b 100644 --- a/test/performance/fixtures/perf-utils.ts +++ b/test/performance/fixtures/perf-utils.ts @@ -59,7 +59,9 @@ export class PerfUtils { this.page.getByRole( 'button', { name: 'Saved' } ) ).toBeDisabled(); - return this.page.url(); + const postId = new URL( this.page.url() ).searchParams.get( 'post' ); + + return postId; } /** diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index da20e3c3e667b5..d5ff40570afd78 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -48,12 +48,12 @@ test.describe( 'Post Editor Performance', () => { } ); test.describe( 'Loading', () => { - let draftURL = null; + let draftId = null; test( 'Setup the test post', async ( { admin, perfUtils } ) => { await admin.createNewPost(); await perfUtils.loadBlocksForLargePost(); - draftURL = await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); } ); const samples = 10; @@ -61,12 +61,12 @@ test.describe( 'Post Editor Performance', () => { const iterations = samples + throwaway; for ( let i = 1; i <= iterations; i++ ) { test( `Run the test (${ i } of ${ iterations })`, async ( { - page, + admin, perfUtils, metrics, } ) => { // Open the test draft. - await page.goto( draftURL ); + await admin.editPost( draftId ); const canvas = await perfUtils.getCanvas(); // Wait for the first block. @@ -92,17 +92,17 @@ test.describe( 'Post Editor Performance', () => { } ); test.describe( 'Typing', () => { - let draftURL = null; + let draftId = null; test( 'Setup the test post', async ( { admin, perfUtils, editor } ) => { await admin.createNewPost(); await perfUtils.loadBlocksForLargePost(); await editor.insertBlock( { name: 'core/paragraph' } ); - draftURL = await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { - await page.goto( draftURL ); + test( 'Run the test', async ( { admin, perfUtils, metrics } ) => { + await admin.editPost( draftId ); await perfUtils.disableAutosave(); const canvas = await perfUtils.getCanvas(); @@ -145,16 +145,16 @@ test.describe( 'Post Editor Performance', () => { } ); test.describe( 'Typing within containers', () => { - let draftURL = null; + let draftId = null; test( 'Set up the test post', async ( { admin, perfUtils } ) => { await admin.createNewPost(); await perfUtils.loadBlocksForSmallPostWithContainers(); - draftURL = await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { - await page.goto( draftURL ); + test( 'Run the test', async ( { admin, perfUtils, metrics } ) => { + await admin.editPost( draftId ); await perfUtils.disableAutosave(); const canvas = await perfUtils.getCanvas(); @@ -201,16 +201,16 @@ test.describe( 'Post Editor Performance', () => { } ); test.describe( 'Selecting blocks', () => { - let draftURL = null; + let draftId = null; test( 'Set up the test post', async ( { admin, perfUtils } ) => { await admin.createNewPost(); await perfUtils.load1000Paragraphs(); - draftURL = await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { - await page.goto( draftURL ); + test( 'Run the test', async ( { admin, page, perfUtils, metrics } ) => { + await admin.editPost( draftId ); await perfUtils.disableAutosave(); const canvas = await perfUtils.getCanvas(); @@ -251,16 +251,16 @@ test.describe( 'Post Editor Performance', () => { } ); test.describe( 'Opening persistent List View', () => { - let draftURL = null; + let draftId = null; test( 'Set up the test page', async ( { admin, perfUtils } ) => { await admin.createNewPost(); await perfUtils.load1000Paragraphs(); - draftURL = await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { - await page.goto( draftURL ); + test( 'Run the test', async ( { page, admin, perfUtils, metrics } ) => { + await admin.editPost( draftId ); await perfUtils.disableAutosave(); const listViewToggle = page.getByRole( 'button', { @@ -301,17 +301,17 @@ test.describe( 'Post Editor Performance', () => { } ); test.describe( 'Opening Inserter', () => { - let draftURL = null; + let draftId = null; test( 'Set up the test page', async ( { admin, perfUtils } ) => { await admin.createNewPost(); await perfUtils.load1000Paragraphs(); - draftURL = await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + test( 'Run the test', async ( { page, admin, perfUtils, metrics } ) => { // Go to the test page. - await page.goto( draftURL ); + await admin.editPost( draftId ); await perfUtils.disableAutosave(); const globalInserterToggle = page.getByRole( 'button', { name: 'Toggle block inserter', @@ -357,17 +357,17 @@ test.describe( 'Post Editor Performance', () => { } ); test.describe( 'Searching Inserter', () => { - let draftURL = null; + let draftId = null; test( 'Set up the test page', async ( { admin, perfUtils } ) => { await admin.createNewPost(); await perfUtils.load1000Paragraphs(); - draftURL = await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + test( 'Run the test', async ( { page, admin, perfUtils, metrics } ) => { // Go to the test page. - await page.goto( draftURL ); + await admin.editPost( draftId ); await perfUtils.disableAutosave(); const globalInserterToggle = page.getByRole( 'button', { name: 'Toggle block inserter', @@ -413,17 +413,17 @@ test.describe( 'Post Editor Performance', () => { } ); test.describe( 'Hovering Inserter items', () => { - let draftURL = null; + let draftId = null; test( 'Set up the test page', async ( { admin, perfUtils } ) => { await admin.createNewPost(); await perfUtils.load1000Paragraphs(); - draftURL = await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { + test( 'Run the test', async ( { page, admin, perfUtils, metrics } ) => { // Go to the test page. - await page.goto( draftURL ); + await admin.editPost( draftId ); await perfUtils.disableAutosave(); const globalInserterToggle = page.getByRole( 'button', { diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index 28a1cbb0ecde29..38bcceb14edd61 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -27,6 +27,7 @@ const results = { inserterHover: [], inserterSearch: [], listViewOpen: [], + navigate: [], }; test.describe( 'Site Editor Performance', () => { @@ -57,19 +58,13 @@ test.describe( 'Site Editor Performance', () => { } ); test.describe( 'Loading', () => { - let draftURL = null; + let draftId = null; - test( 'Setup the test page', async ( { page, admin, perfUtils } ) => { + test( 'Setup the test page', async ( { admin, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); await perfUtils.loadBlocksForLargePost(); - await perfUtils.saveDraft(); - await admin.visitSiteEditor( { - postId: new URL( page.url() ).searchParams.get( 'post' ), - postType: 'page', - } ); - - draftURL = page.url(); + draftId = await perfUtils.saveDraft(); } ); const samples = 10; @@ -77,15 +72,18 @@ test.describe( 'Site Editor Performance', () => { const iterations = samples + throwaway; for ( let i = 1; i <= iterations; i++ ) { test( `Run the test (${ i } of ${ iterations })`, async ( { - page, + admin, perfUtils, metrics, } ) => { // Go to the test draft. - await page.goto( draftURL ); - const canvas = await perfUtils.getCanvas(); + await admin.visitSiteEditor( { + postId: draftId, + postType: 'page', + } ); // Wait for the first block. + const canvas = await perfUtils.getCanvas(); await canvas.locator( '.wp-block' ).first().waitFor(); // Get the durations. @@ -106,45 +104,27 @@ test.describe( 'Site Editor Performance', () => { } ); } } ); + test.describe( 'Typing', () => { - let draftURL = null; - - test( 'Setup the test post', async ( { - page, - admin, - editor, - perfUtils, - } ) => { + let draftId = null; + + test( 'Setup the test post', async ( { admin, editor, perfUtils } ) => { await admin.createNewPost( { postType: 'page' } ); await perfUtils.loadBlocksForLargePost(); await editor.insertBlock( { name: 'core/paragraph' } ); - await perfUtils.saveDraft(); + draftId = await perfUtils.saveDraft(); + } ); + + test( 'Run the test', async ( { admin, perfUtils, metrics } ) => { + // Go to the test draft. await admin.visitSiteEditor( { - postId: new URL( page.url() ).searchParams.get( 'post' ), + postId: draftId, postType: 'page', } ); - draftURL = page.url(); - } ); - test( 'Run the test', async ( { page, perfUtils, metrics } ) => { - await page.goto( draftURL ); - await perfUtils.disableAutosave(); - - // Wait for the loader overlay to disappear. This is necessary - // because the overlay is still visible for a while after the editor - // canvas is ready, and we don't want it to affect the typing - // timings. - await page - .locator( - // Spinner was used instead of the progress bar in an earlier version of the site editor. - '.edit-site-canvas-loader, .edit-site-canvas-spinner' - ) - .waitFor( { state: 'hidden' } ); - - const canvas = await perfUtils.getCanvas(); - // Enter edit mode (second click is needed for the legacy edit mode). + const canvas = await perfUtils.getCanvas(); await canvas.locator( 'body' ).click(); await canvas .getByRole( 'document', { name: /Block:( Post)? Content/ } ) @@ -187,6 +167,45 @@ test.describe( 'Site Editor Performance', () => { } } ); } ); + + test.describe( 'Navigating', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentythree' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + const iterations = 5; + for ( let i = 1; i <= iterations; i++ ) { + test( `Run the test (${ i } of ${ iterations })`, async ( { + admin, + page, + metrics, + } ) => { + await admin.visitSiteEditor( { + path: '/wp_template', + } ); + + // Start tracing. + await metrics.startTracing(); + + await page + .getByRole( 'button', { name: 'Single Posts' } ) + .click(); + + // Stop tracing. + await metrics.stopTracing(); + + // Get the durations. + const [ mouseClickEvents ] = metrics.getClickEventDurations(); + + // Save the results. + results.navigate.push( mouseClickEvents[ 0 ] ); + } ); + } + } ); } ); /* eslint-enable playwright/no-conditional-in-test, playwright/expect-expect */