From 8e2b33290dbb5169a045f9082acb8c9b15393df7 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 10 Jan 2024 16:47:38 +0000 Subject: [PATCH] Merge changes published in the Gutenberg plugin "release/17.5" branch --- .eslintrc.js | 4 - .github/dependabot.yml | 13 + .github/workflows/build-plugin-zip.yml | 8 +- .github/workflows/bundle-size.yml | 2 +- .github/workflows/end2end-test.yml | 2 +- .github/workflows/php-changes-detection.yml | 2 +- .github/workflows/publish-npm-packages.yml | 4 +- .github/workflows/pull-request-automation.yml | 2 +- .github/workflows/rnmobile-android-runner.yml | 8 +- .github/workflows/static-checks.yml | 2 +- .../upload-release-to-plugin-repo.yml | 4 +- LICENSE.md | 2 +- README.md | 2 +- changelog.txt | 311 ++++- docs/contributors/documentation/README.md | 4 +- .../explanations/architecture/key-concepts.md | 4 +- docs/explanations/architecture/styles.md | 4 +- .../user-interface/block-design.md | 2 +- docs/getting-started/devenv/README.md | 7 + docs/getting-started/fundamentals/README.md | 3 +- .../markup-representation-block.md | 2 +- .../fundamentals/static-dynamic-rendering.md | 170 +++ docs/getting-started/glossary.md | 2 +- docs/getting-started/quick-start-guide.md | 10 +- docs/getting-started/tutorial.md | 37 +- docs/how-to-guides/README.md | 2 +- .../applying-styles-with-stylesheets.md | 2 +- .../curating-the-editor-experience.md | 459 ------- .../curating-the-editor-experience/README.md | 25 + .../block-locking.md | 69 ++ .../disable-editor-functionality.md | 89 ++ .../filters-and-hooks.md | 109 ++ .../patterns.md | 91 ++ .../theme-json.md | 210 ++++ docs/how-to-guides/metabox.md | 2 +- docs/how-to-guides/themes/README.md | 4 +- ...-json.md => global-settings-and-styles.md} | 26 - docs/how-to-guides/themes/theme-support.md | 4 +- docs/manifest.json | 42 +- .../block-api/block-supports.md | 6 +- docs/reference-guides/core-blocks.md | 6 +- .../data/data-core-block-editor.md | 24 - .../theme-json-reference/README.md | 2 +- .../theme-json-reference/theme-json-living.md | 2 +- docs/toc.json | 27 +- gutenberg.php | 2 +- lib/block-supports/background.php | 1 + lib/block-supports/behaviors.php | 260 ---- lib/block-supports/layout.php | 4 +- lib/blocks.php | 27 - lib/class-wp-theme-json-gutenberg.php | 7 +- lib/class-wp-theme-json-schema-gutenberg.php | 202 ---- .../class-wp-navigation-block-renderer.php | 50 +- lib/experimental/blocks.php | 2 +- lib/experimental/connection-sources/index.php | 8 +- .../class-wp-font-family-utils.php | 15 - .../font-library/class-wp-font-family.php | 11 - .../font-library/class-wp-font-library.php | 50 +- ...class-wp-rest-font-families-controller.php | 149 +-- .../fonts/font-library/font-library.php | 13 + .../class-wp-directive-processor.php | 191 ++- .../class-wp-interactivity-initial-state.php | 82 ++ .../class-wp-interactivity-store.php | 69 -- .../directive-processing.php | 322 ++++- .../interactivity-api/directives/wp-bind.php | 7 +- .../interactivity-api/directives/wp-class.php | 7 +- .../directives/wp-context.php | 16 +- .../directives/wp-interactive.php | 44 + .../interactivity-api/directives/wp-style.php | 9 +- .../interactivity-api/directives/wp-text.php | 5 +- .../interactivity-api/initial-state.php | 29 + lib/experimental/interactivity-api/store.php | 28 - .../modules/class-gutenberg-modules.php | 214 +++- lib/experiments-page.php | 6 +- lib/load.php | 7 +- package-lock.json | 1068 ++++++++--------- package.json | 6 +- packages/a11y/CHANGELOG.md | 2 - packages/a11y/package.json | 2 +- packages/annotations/CHANGELOG.md | 2 - packages/annotations/package.json | 2 +- packages/api-fetch/CHANGELOG.md | 2 - packages/api-fetch/package.json | 2 +- packages/autop/CHANGELOG.md | 2 - packages/autop/package.json | 2 +- .../CHANGELOG.md | 2 - .../package.json | 2 +- packages/babel-plugin-makepot/CHANGELOG.md | 2 - packages/babel-plugin-makepot/package.json | 2 +- packages/babel-preset-default/CHANGELOG.md | 2 - packages/babel-preset-default/package.json | 2 +- packages/base-styles/CHANGELOG.md | 2 - packages/base-styles/package.json | 2 +- packages/blob/CHANGELOG.md | 2 - packages/blob/package.json | 2 +- packages/block-directory/CHANGELOG.md | 2 - packages/block-directory/package.json | 2 +- packages/block-editor/CHANGELOG.md | 2 - packages/block-editor/package.json | 2 +- .../src/components/block-canvas/index.js | 2 - .../src/components/block-canvas/style.scss | 6 + .../block-draggable/index.native.js | 5 - .../block-draggable/test/helpers.native.js | 15 +- .../use-scroll-when-dragging.native.js | 2 +- .../src/components/block-inspector/index.js | 4 +- .../block-list/block-list-item-cell.native.js | 6 +- .../src/components/block-list/block.native.js | 17 +- .../src/components/block-list/index.native.js | 13 +- .../block-list/use-in-between-inserter.js | 5 +- .../use-scroll-upon-insertion.native.js | 52 + .../src/components/block-lock/toolbar.js | 62 +- .../components/block-patterns-list/index.js | 13 +- .../src/components/block-toolbar/index.js | 3 +- .../global-styles/dimensions-panel.js | 4 +- .../get-global-styles-changes.js} | 107 +- .../src/components/global-styles/index.js | 1 + .../test/get-global-styles-changes.js} | 81 +- .../src/components/height-control/README.md | 4 +- .../src/components/height-control/index.js | 4 + .../src/components/index.native.js | 4 +- .../inserter-draggable-blocks/index.js | 4 +- .../block-patterns-explorer/pattern-list.js | 4 +- .../pattern-category-previews.js | 4 +- .../block-patterns-tab/patterns-filter.js | 28 +- .../use-pattern-categories.js | 4 +- .../inserter/block-patterns-tab/utils.js | 23 +- .../inserter/hooks/use-patterns-state.js | 4 +- .../src/components/inserter/index.js | 1 - .../src/components/inserter/library.js | 10 +- .../src/components/inserter/menu.js | 2 - .../src/components/inserter/tabs.js | 4 +- .../src/components/list-view/block.js | 3 +- .../src/components/list-view/style.scss | 6 + .../media-upload-progress/constants.js | 6 + .../media-upload-progress/index.native.js | 80 +- .../test/index.native.js | 4 +- .../src/components/navigable-toolbar/index.js | 3 +- .../components/offline-status/index.native.js | 48 - .../rich-text/native/index.native.js | 10 +- .../test/__snapshots__/index.native.js.snap | 6 +- .../rich-text/native/test/index.native.js | 58 +- .../rich-text/use-mark-persistent.js | 3 +- .../src/components/url-input/index.js | 22 +- .../components/use-block-commands/index.js | 39 +- .../src/components/use-on-block-drop/index.js | 17 +- .../use-on-block-drop/test/index.js | 22 +- .../components/writing-flow/use-tab-nav.js | 14 +- packages/block-editor/src/hooks/background.js | 11 + packages/block-editor/src/hooks/color.js | 18 +- packages/block-editor/src/hooks/layout.js | 2 +- packages/block-editor/src/store/actions.js | 15 - .../block-editor/src/store/private-actions.js | 15 + .../src/store/private-selectors.js | 11 + packages/block-editor/src/store/selectors.js | 18 +- packages/block-editor/src/store/utils.js | 4 +- packages/block-editor/src/style.scss | 1 + packages/block-library/CHANGELOG.md | 2 - packages/block-library/package.json | 2 +- packages/block-library/src/audio/edit.js | 27 +- .../test/__snapshots__/edit.native.js.snap | 12 + .../src/audio/test/edit.native.js | 29 + packages/block-library/src/block/edit.js | 117 +- packages/block-library/src/button/style.scss | 12 +- packages/block-library/src/file/edit.js | 41 +- packages/block-library/src/gallery/block.json | 4 + packages/block-library/src/gallery/edit.js | 13 +- packages/block-library/src/gallery/index.php | 18 + .../block-library/src/gallery/transforms.js | 3 +- .../block-library/src/image/edit.native.js | 3 + packages/block-library/src/image/image.js | 2 +- packages/block-library/src/image/index.php | 6 - .../src/media-text/media-container.native.js | 4 +- .../block-library/src/navigation/constants.js | 2 + .../src/navigation/edit/index.js | 12 +- .../block-library/src/navigation/editor.scss | 2 +- .../block-library/src/navigation/style.scss | 34 +- packages/block-library/src/navigation/view.js | 32 +- .../src/post-featured-image/block.json | 4 + .../src/post-featured-image/edit.js | 33 +- .../src/post-featured-image/index.php | 31 + packages/block-library/src/search/block.json | 4 - packages/block-library/src/search/edit.js | 8 +- packages/block-library/src/search/index.php | 10 +- packages/block-library/src/search/style.scss | 56 +- packages/block-library/src/site-logo/edit.js | 7 +- packages/block-library/src/table/editor.scss | 3 - .../block-library/src/template-part/index.php | 6 + packages/block-library/src/video/edit.js | 27 +- .../CHANGELOG.md | 2 - .../package.json | 2 +- .../CHANGELOG.md | 2 - .../package.json | 2 +- packages/blocks/CHANGELOG.md | 2 - packages/blocks/package.json | 2 +- packages/browserslist-config/CHANGELOG.md | 2 - packages/browserslist-config/package.json | 2 +- packages/commands/CHANGELOG.md | 2 - packages/commands/package.json | 2 +- packages/components/CHANGELOG.md | 12 +- packages/components/package.json | 6 +- .../alignment-matrix-control/test/index.tsx | 26 +- .../components/src/base-control/index.tsx | 37 +- .../src/context/wordpress-component.ts | 17 +- .../src/duotone-picker/duotone-picker.tsx | 10 +- packages/components/src/index.native.js | 3 +- .../styles/input-control-styles.tsx | 18 +- .../components/src/input-control/types.ts | 2 +- .../components/src/mobile/image/constants.js | 1 + .../src/mobile/image/index.native.js | 73 +- .../src/mobile/image/style.native.scss | 44 +- .../keyboard-aware-flat-list/index.android.js | 55 +- .../keyboard-aware-flat-list/index.ios.js | 156 +-- ...ive.js => use-scroll-to-section.native.js} | 52 +- .../test/use-scroll.native.js | 71 ++ .../use-scroll-to-element.native.js | 41 + ...ive.js => use-scroll-to-section.native.js} | 49 +- .../use-scroll.native.js | 100 ++ packages/components/src/modal/index.tsx | 2 +- packages/components/src/navigator/styles.ts | 6 +- .../components/src/number-control/README.md | 4 +- .../components/src/number-control/index.tsx | 8 +- .../components/src/number-control/types.ts | 2 +- .../styles/select-control-styles.ts | 38 +- .../src/snackbar/stories/index.story.tsx | 12 +- packages/components/src/snackbar/style.scss | 7 +- .../src/toggle-group-control/test/index.tsx | 103 +- packages/components/src/tooltip/index.tsx | 2 +- .../components/src/tooltip/test/index.tsx | 616 ++++++---- .../components/src/unit-control/index.tsx | 2 +- packages/compose/CHANGELOG.md | 2 - packages/compose/package.json | 2 +- packages/core-commands/CHANGELOG.md | 2 - packages/core-commands/package.json | 2 +- packages/core-data/CHANGELOG.md | 2 - packages/core-data/package.json | 2 +- packages/core-data/src/footnotes/index.js | 8 + packages/core-data/src/index.js | 8 +- .../CHANGELOG.md | 14 +- .../README.md | 2 + .../block-templates/render.php.mustache | 5 - .../index.js | 6 + .../package.json | 2 +- .../plugin-templates/$slug.php.mustache | 11 +- .../CHANGELOG.md | 2 - .../package.json | 2 +- packages/create-block/CHANGELOG.md | 4 +- packages/create-block/lib/init-block.js | 2 + packages/create-block/lib/scaffold.js | 2 + packages/create-block/package.json | 2 +- packages/customize-widgets/CHANGELOG.md | 2 - packages/customize-widgets/package.json | 2 +- packages/data-controls/CHANGELOG.md | 2 - packages/data-controls/package.json | 2 +- packages/data/CHANGELOG.md | 2 - packages/data/package.json | 2 +- packages/dataviews/CHANGELOG.md | 2 - packages/dataviews/README.md | 3 +- packages/dataviews/package.json | 2 +- packages/dataviews/src/add-filter.js | 18 +- packages/dataviews/src/dataviews.js | 24 +- .../dataviews/src/dropdown-menu-helper.js | 9 +- packages/dataviews/src/filter-summary.js | 10 +- packages/dataviews/src/filters.js | 16 +- packages/dataviews/src/index.js | 1 + packages/dataviews/src/item-actions.js | 10 +- packages/dataviews/src/pagination.js | 10 +- packages/dataviews/src/search.js | 8 +- packages/dataviews/src/style.scss | 182 ++- packages/dataviews/src/utils.js | 51 + packages/dataviews/src/view-actions.js | 45 +- packages/dataviews/src/view-grid.js | 4 +- packages/dataviews/src/view-list.js | 68 +- packages/dataviews/src/view-table.js | 222 ++-- packages/date/CHANGELOG.md | 2 - packages/date/package.json | 2 +- .../CHANGELOG.md | 12 +- .../README.md | 133 +- .../lib/.eslintrc.json | 2 - .../lib/index.js | 170 ++- .../lib/types.d.ts | 3 + .../lib/util.js | 24 +- .../package.json | 2 +- .../test/__snapshots__/build.js.snap | 363 ++++++ .../test/build.js | 164 +-- .../fixtures/combine-assets/webpack.config.js | 3 + .../fixtures/cyclic-dependency-graph/a.js | 13 + .../fixtures/cyclic-dependency-graph/b.js | 10 + .../fixtures/cyclic-dependency-graph/index.js | 8 + .../cyclic-dependency-graph/webpack.config.js | 8 + .../cyclic-dynamic-dependency-graph/a.js | 13 + .../cyclic-dynamic-dependency-graph/b.js | 10 + .../cyclic-dynamic-dependency-graph/index.js | 9 + .../webpack.config.js | 8 + .../fixtures/dynamic-import/webpack.config.js | 8 +- .../webpack.config.js | 8 +- .../has-extension-suffix/webpack.config.js | 8 +- .../test/fixtures/module-renames/index.js | 7 + .../fixtures/module-renames/webpack.config.js | 32 + .../webpack.config.js | 3 + .../option-output-filename/webpack.config.js | 3 + .../test/fixtures/overrides/webpack.config.js | 10 + .../runtime-chunk-single/webpack.config.js | 8 +- .../fixtures/style-imports/webpack.config.js | 6 +- .../fixtures/wordpress-interactivity/index.js | 12 + .../wordpress-interactivity/webpack.config.js | 8 + .../wordpress-require/webpack.config.js | 8 +- .../test/fixtures/wordpress/webpack.config.js | 8 +- packages/deprecated/CHANGELOG.md | 2 - packages/deprecated/package.json | 2 +- packages/docgen/CHANGELOG.md | 2 - packages/docgen/package.json | 2 +- packages/dom-ready/CHANGELOG.md | 2 - packages/dom-ready/package.json | 2 +- packages/dom/CHANGELOG.md | 2 - packages/dom/package.json | 2 +- .../e2e-test-utils-playwright/CHANGELOG.md | 2 - .../e2e-test-utils-playwright/package.json | 2 +- .../src/editor/get-blocks.ts | 14 +- .../src/editor/insert-block.ts | 42 +- .../src/editor/set-is-fixed-toolbar.ts | 2 +- packages/e2e-test-utils/CHANGELOG.md | 2 - packages/e2e-test-utils/package.json | 2 +- .../src/disable-pre-publish-checks.js | 2 +- .../src/enable-pre-publish-checks.js | 6 +- packages/e2e-test-utils/src/inserter.js | 3 +- packages/e2e-test-utils/src/list-view.js | 4 +- packages/e2e-tests/CHANGELOG.md | 2 - packages/e2e-tests/jest.config.js | 1 - packages/e2e-tests/package.json | 3 +- ...ock-editor-keyboard-shortcuts.test.js.snap | 153 --- .../editor/various/allowed-patterns.test.js | 74 -- .../block-editor-keyboard-shortcuts.test.js | 110 -- .../specs/editor/various/datepicker.test.js | 148 --- .../editor/various/dropdown-menu.test.js | 143 --- .../specs/editor/various/editor-modes.test.js | 163 --- .../editor/various/inserting-blocks.test.js | 8 +- .../editor/various/invalid-block.test.js | 100 -- .../specs/editor/various/nux.test.js | 158 --- .../specs/editor/various/preferences.test.js | 62 - .../editor/various/publish-panel.test.js | 82 -- .../specs/editor/various/publishing.test.js | 176 --- .../specs/editor/various/scheduling.test.js | 65 - .../specs/editor/various/sidebar.test.js | 171 --- .../specs/editor/various/taxonomies.test.js | 251 ---- .../experiments/experimental-features.js | 39 - .../fixtures/menu-items-request-fixture.json | 84 -- .../specs/widgets/editing-widgets.test.js | 962 --------------- packages/edit-post/CHANGELOG.md | 2 - packages/edit-post/package.json | 2 +- .../src/components/browser-url/index.js | 6 +- .../edit-post/src/components/header/index.js | 39 +- .../src/components/header/style.scss | 6 +- .../components/header/writing-menu/index.js | 12 +- .../edit-post/src/components/layout/index.js | 26 +- .../src/components/layout/index.native.js | 10 +- .../src/components/preferences-modal/index.js | 21 +- .../options/enable-feature.js | 47 +- .../__snapshots__/meta-boxes-section.js.snap | 362 +++--- .../secondary-sidebar/list-view-outline.js | 98 -- .../sidebar/plugin-sidebar/index.js | 10 +- .../src/components/text-editor/style.scss | 6 - packages/edit-post/src/editor.js | 67 +- packages/edit-post/src/editor.native.js | 19 +- .../src/hooks/commands/use-common-commands.js | 24 +- .../edit-post/src/hooks/use-post-history.js | 73 ++ packages/edit-post/src/index.js | 22 +- packages/edit-post/src/index.native.js | 9 +- packages/edit-post/src/store/actions.js | 16 +- packages/edit-post/src/store/selectors.js | 7 +- packages/edit-post/src/store/test/actions.js | 16 +- packages/edit-post/src/style.scss | 5 +- packages/edit-post/src/test/editor.native.js | 17 + packages/edit-site/CHANGELOG.md | 2 - packages/edit-site/package.json | 2 +- .../components/add-new-template/style.scss | 3 +- .../components/block-editor/back-button.js | 6 +- .../block-editor/resizable-editor.js | 2 +- .../src/components/block-editor/style.scss | 3 +- .../block-editor/use-post-link-props.js | 20 + .../block-editor/use-site-editor-settings.js | 53 +- .../src/components/code-editor/style.scss | 4 - .../create-template-part-modal/index.js | 165 +-- .../editor-canvas-container/index.js | 2 +- .../edit-site/src/components/editor/index.js | 20 +- .../confirm-delete-dialog.js | 6 +- .../font-library-modal/context.js | 14 +- .../font-library-modal/font-collection.js | 15 +- .../global-styles/font-library-modal/index.js | 68 +- .../font-library-modal/installed-fonts.js | 6 +- .../font-library-modal/local-fonts.js | 79 +- .../font-library-modal/resolvers.js | 2 +- .../font-library-modal/style.scss | 7 +- .../{tab-layout.js => tab-panel-layout.js} | 12 +- .../font-library-modal/utils/index.js | 30 +- .../test/makeFormDataFromFontFamilies.spec.js | 62 - .../test/makeFormDataFromFontFamily.spec.js | 58 + .../screen-revisions/revisions-buttons.js | 38 +- .../header-edit-mode/document-tools/index.js | 183 +-- .../src/components/header-edit-mode/index.js | 19 +- .../header-edit-mode/mode-switcher/index.js | 4 - .../header-edit-mode/more-menu/index.js | 10 +- .../components/header-edit-mode/style.scss | 47 - .../header-edit-mode/undo-redo/redo.js | 38 - .../header-edit-mode/undo-redo/undo.js | 34 - .../edit-site/src/components/layout/index.js | 4 +- .../edit-site/src/components/list/style.scss | 5 - .../src/components/page-main/index.js | 9 +- .../src/components/page-pages/index.js | 82 +- .../src/components/page-pages/style.scss | 11 +- .../dataviews-pattern-actions.js | 329 +++++ .../page-patterns/dataviews-patterns.js | 380 ++++++ .../page-patterns/duplicate-menu-item.js | 2 +- .../src/components/page-patterns/grid-item.js | 4 +- .../src/components/page-patterns/style.scss | 66 + .../components/page-patterns/use-patterns.js | 39 +- .../src/components/page-templates/index.js | 73 +- .../preferences-modal/enable-feature.js | 14 +- .../preferences-modal/enable-panel-option.js | 23 + .../src/components/preferences-modal/index.js | 132 +- .../edit-site/src/components/routes/link.js | 14 +- .../secondary-sidebar/inserter-sidebar.js | 67 -- .../secondary-sidebar/list-view-sidebar.js | 121 -- .../components/secondary-sidebar/style.scss | 65 - .../sidebar-dataviews/add-new-view.js | 2 +- .../sidebar-dataviews/default-views.js | 29 +- .../src/components/sidebar-dataviews/index.js | 4 +- .../sidebar-edit-mode/default-sidebar.js | 12 - .../global-styles-sidebar.js | 2 +- .../sidebar-edit-mode/plugin-sidebar/index.js | 12 - .../sidebar-navigation-screen-page/index.js | 3 +- .../index.js | 77 ++ .../edit-site/src/components/sidebar/index.js | 19 +- .../src/components/site-hub/style.scss | 7 +- .../src/components/style-book/index.js | 49 +- .../src/components/style-book/style.scss | 6 +- .../hooks/commands/use-edit-mode-commands.js | 17 +- packages/edit-site/src/index.js | 17 +- packages/edit-site/src/store/actions.js | 14 +- .../edit-site/src/store/private-actions.js | 4 +- packages/edit-site/src/store/test/actions.js | 16 +- packages/edit-site/src/style.scss | 1 - .../edit-site/src/utils/get-is-list-page.js | 5 +- packages/edit-widgets/CHANGELOG.md | 2 - packages/edit-widgets/package.json | 2 +- .../src/store/private-selectors.js | 19 +- packages/edit-widgets/src/store/reducer.js | 11 + packages/editor/CHANGELOG.md | 2 - packages/editor/package.json | 2 +- .../src/components/document-bar/index.js | 67 +- .../src/components/document-outline/index.js | 49 +- .../components/document-outline/style.scss | 12 + .../src/components/document-tools}/index.js | 102 +- .../src/components/document-tools}/style.scss | 34 +- .../src/components/editor-canvas/index.js | 19 +- .../src/components/editor-canvas/style.scss | 5 + .../components/entities-saved-states/index.js | 4 +- .../entities-saved-states/style.scss | 4 + .../editor/src/components/index.native.js | 1 + .../src/components/inserter-sidebar/index.js} | 14 +- .../components/inserter-sidebar/style.scss | 22 + .../components/list-view-sidebar/index.js} | 16 +- .../list-view-sidebar/list-view-outline.js | 37 + .../components/list-view-sidebar}/style.scss | 64 +- .../components/offline-status/index.native.js | 101 ++ .../offline-status/style.native.scss | 0 .../offline-status/test/index.native.js | 108 ++ .../components/post-featured-image/index.js | 10 +- .../src/components/post-saved-state/index.js | 16 +- .../components/post-template/block-theme.js | 3 +- .../src/components/post-view-link/index.js | 13 +- .../src/components/post-visibility/check.js | 25 +- .../components/post-visibility/test/check.js | 37 +- .../src/components/preview-dropdown/index.js | 17 +- .../src/components/provider/index.native.js | 31 +- .../provider/use-block-editor-settings.js | 40 +- packages/editor/src/private-apis.js | 6 + packages/editor/src/store/actions.js | 11 +- .../editor/src/store/private-selectors.js | 15 +- packages/editor/src/store/reducer.js | 12 + packages/editor/src/store/selectors.js | 4 +- packages/editor/src/style.scss | 4 + .../editor/src/utils/media-upload/index.js | 13 +- packages/element/CHANGELOG.md | 2 - packages/element/package.json | 2 +- packages/env/CHANGELOG.md | 2 - packages/env/package.json | 2 +- packages/escape-html/CHANGELOG.md | 2 - packages/escape-html/package.json | 2 +- packages/eslint-plugin/CHANGELOG.md | 2 - packages/eslint-plugin/package.json | 2 +- packages/format-library/CHANGELOG.md | 2 - packages/format-library/package.json | 3 +- packages/format-library/src/lock-unlock.js | 10 + .../format-library/src/text-color/inline.js | 59 +- .../format-library/src/text-color/style.scss | 23 +- packages/hooks/CHANGELOG.md | 2 - packages/hooks/package.json | 2 +- packages/html-entities/CHANGELOG.md | 2 - packages/html-entities/package.json | 2 +- packages/i18n/CHANGELOG.md | 2 - packages/i18n/package.json | 2 +- packages/icons/CHANGELOG.md | 2 - packages/icons/package.json | 2 +- packages/icons/src/library/offline.js | 8 +- packages/interactivity/CHANGELOG.md | 2 - .../interactivity/docs/1-getting-started.md | 16 +- packages/interactivity/package.json | 2 +- packages/interface/CHANGELOG.md | 2 - packages/interface/package.json | 2 +- .../components/complementary-area/index.js | 52 +- .../preferences-modal-section/index.js | 4 +- .../preferences-modal-section/style.scss | 4 + .../components/preferences-modal/README.md | 2 +- packages/is-shallow-equal/CHANGELOG.md | 2 - packages/is-shallow-equal/package.json | 2 +- packages/jest-console/CHANGELOG.md | 2 - packages/jest-console/package.json | 2 +- packages/jest-preset-default/CHANGELOG.md | 2 - packages/jest-preset-default/package.json | 2 +- packages/jest-puppeteer-axe/CHANGELOG.md | 2 - packages/jest-puppeteer-axe/package.json | 2 +- packages/keyboard-shortcuts/CHANGELOG.md | 2 - packages/keyboard-shortcuts/package.json | 2 +- packages/keycodes/CHANGELOG.md | 2 - packages/keycodes/package.json | 2 +- packages/lazy-import/CHANGELOG.md | 2 - packages/lazy-import/package.json | 2 +- packages/list-reusable-blocks/CHANGELOG.md | 2 - packages/list-reusable-blocks/package.json | 2 +- packages/media-utils/CHANGELOG.md | 2 - packages/media-utils/package.json | 2 +- packages/notices/CHANGELOG.md | 2 - packages/notices/package.json | 2 +- .../npm-package-json-lint-config/CHANGELOG.md | 2 - .../npm-package-json-lint-config/package.json | 2 +- packages/nux/CHANGELOG.md | 2 - packages/nux/package.json | 2 +- packages/patterns/CHANGELOG.md | 2 - packages/patterns/package.json | 2 +- .../src/components/create-pattern-modal.js | 189 +-- .../src/components/duplicate-pattern-modal.js | 50 +- packages/patterns/src/private-apis.js | 12 +- packages/plugins/CHANGELOG.md | 2 - packages/plugins/package.json | 2 +- packages/postcss-plugins-preset/CHANGELOG.md | 2 - packages/postcss-plugins-preset/package.json | 2 +- packages/postcss-themes/CHANGELOG.md | 2 - packages/postcss-themes/package.json | 2 +- packages/preferences-persistence/CHANGELOG.md | 2 - packages/preferences-persistence/package.json | 2 +- .../convert-editor-settings.js | 47 + .../preferences-package-data/index.js | 5 +- .../test/convert-editor-settings.js | 22 + .../preferences-package-data/test/index.js | 13 +- packages/preferences/CHANGELOG.md | 2 - packages/preferences/package.json | 2 +- packages/prettier-config/CHANGELOG.md | 2 - packages/prettier-config/package.json | 2 +- packages/primitives/CHANGELOG.md | 2 - packages/primitives/package.json | 2 +- packages/priority-queue/CHANGELOG.md | 2 - packages/priority-queue/package.json | 2 +- packages/private-apis/CHANGELOG.md | 2 - packages/private-apis/package.json | 2 +- packages/private-apis/src/implementation.js | 1 + .../CHANGELOG.md | 2 - .../package.json | 2 +- packages/react-i18n/CHANGELOG.md | 2 - packages/react-i18n/package.json | 2 +- .../ReactNativeAztec/ReactAztecText.java | 52 +- packages/react-native-aztec/package.json | 2 +- .../react-native-aztec/src/AztecInputState.js | 29 +- .../src/test/AztecInputState.test.js | 119 +- .../GutenbergBridgeJS2Parent.java | 1 + .../WPAndroidGlue/DeferredEventEmitter.java | 6 + .../mobile/WPAndroidGlue/GutenbergProps.kt | 2 +- .../WPAndroidGlue/WPAndroidGlueCode.java | 28 +- packages/react-native-bridge/index.js | 11 +- .../react-native-bridge/ios/Gutenberg.swift | 1 + packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 5 + .../gutenberg-editor-device-actions.test.js | 4 + .../gutenberg-editor-heading-@canary.test.js | 63 - .../gutenberg-editor-paragraph.test.js | 143 --- ...enberg-editor-writing-flow-@canary.test.js | 333 +++++ .../__device-tests__/pages/editor-page.js | 108 +- .../main/java/com/gutenberg/MainActivity.java | 9 + .../GutenbergViewController.swift | 5 +- packages/react-native-editor/ios/Podfile.lock | 8 +- packages/react-native-editor/package.json | 2 +- .../CHANGELOG.md | 2 - .../package.json | 2 +- packages/redux-routine/CHANGELOG.md | 2 - packages/redux-routine/package.json | 2 +- packages/reusable-blocks/CHANGELOG.md | 2 - packages/reusable-blocks/package.json | 2 +- packages/rich-text/CHANGELOG.md | 2 - packages/rich-text/package.json | 2 +- packages/rich-text/src/create.js | 13 + packages/router/CHANGELOG.md | 2 - packages/router/package.json | 2 +- packages/scripts/CHANGELOG.md | 4 +- packages/scripts/README.md | 14 +- packages/scripts/config/webpack.config.js | 200 ++- packages/scripts/package.json | 2 +- packages/scripts/scripts/build.js | 4 + packages/scripts/scripts/start.js | 4 + packages/scripts/utils/block-json.js | 41 + packages/scripts/utils/config.js | 253 ++-- packages/scripts/utils/index.js | 6 + packages/server-side-render/CHANGELOG.md | 2 - packages/server-side-render/package.json | 2 +- packages/shortcode/CHANGELOG.md | 2 - packages/shortcode/package.json | 2 +- packages/style-engine/CHANGELOG.md | 2 - ...ng-the-style-engine-with-block-supports.md | 2 +- packages/style-engine/package.json | 2 +- packages/style-engine/src/test/index.js | 87 ++ packages/stylelint-config/CHANGELOG.md | 2 - packages/stylelint-config/package.json | 2 +- packages/sync/CHANGELOG.md | 2 - packages/sync/package.json | 2 +- packages/token-list/CHANGELOG.md | 2 - packages/token-list/package.json | 2 +- packages/undo-manager/CHANGELOG.md | 2 - packages/undo-manager/package.json | 2 +- packages/url/CHANGELOG.md | 2 - packages/url/package.json | 2 +- packages/viewport/CHANGELOG.md | 2 - packages/viewport/package.json | 2 +- packages/warning/CHANGELOG.md | 2 - packages/warning/package.json | 2 +- packages/widgets/CHANGELOG.md | 2 - packages/widgets/package.json | 2 +- packages/wordcount/CHANGELOG.md | 2 - packages/wordcount/package.json | 2 +- ...=> react-native+0.71.11+001+initial.patch} | 0 ...act-native+0.71.11+002+boost-podspec.patch | 13 + phpunit/block-supports/background-test.php | 27 +- ...ss-wp-interactivity-initial-state-test.php | 115 ++ .../class-wp-interactivity-store-test.php | 186 --- .../directive-processing-test.php | 559 +++++++-- .../directives/wp-bind-test.php | 12 +- .../directives/wp-class-test.php | 33 +- .../directives/wp-context-test.php | 61 +- .../directives/wp-style-test.php | 27 +- .../directives/wp-text-test.php | 12 +- phpunit/style-engine/style-engine-test.php | 14 +- .../wpFontFamilyUtils/hasFontMimeType.php | 61 - .../fonts/font-library/wpFontLibrary/base.php | 26 + .../wpFontLibrary/getFontCollection.php | 8 +- .../wpFontLibrary/getFontCollections.php | 26 +- .../wpFontLibrary/getFontsDir.php | 2 +- .../wpFontLibrary/getMimeTypes.php | 2 +- .../wpFontLibrary/registerFontCollection.php | 6 +- .../wpFontLibrary/setUploadDir.php | 2 +- .../unregisterFontCollection.php | 54 + .../installFonts.php | 287 ++--- schemas/json/block.json | 5 + test/e2e/specs/editor/blocks/image.spec.js | 6 +- test/e2e/specs/editor/blocks/links.spec.js | 4 +- test/e2e/specs/editor/blocks/missing.spec.js | 20 +- .../navigation-frontend-interactivity.spec.js | 6 +- .../editor/plugins/post-type-locking.spec.js | 41 - .../editor/various/allowed-patterns.spec.js | 86 ++ .../block-editor-keyboard-shortcuts.spec.js | 220 ++++ .../editor/various/block-locking.spec.js | 6 + .../editor/various/block-renaming.spec.js | 2 +- .../editor/various/core-settings.spec.js | 36 +- .../specs/editor/various/datepicker.spec.js | 114 ++ .../editor/various/dropdown-menu.spec.js | 62 + .../specs/editor/various/editor-modes.spec.js | 158 +++ .../editor/various/invalid-block.spec.js | 119 ++ test/e2e/specs/editor/various/nux.spec.js | 138 +++ .../specs/editor/various/pref-modal.spec.js | 8 +- .../specs/editor/various/preferences.spec.js | 50 + .../editor/various/publish-button.spec.js | 16 +- .../editor/various/publish-panel.spec.js | 56 + .../specs/editor/various/publishing.spec.js | 164 +++ .../specs/editor/various/scheduling.spec.js | 90 ++ test/e2e/specs/editor/various/sidebar.spec.js | 136 +++ .../specs/editor/various/taxonomies.spec.js | 136 +++ test/e2e/specs/site-editor/list-view.spec.js | 5 +- test/e2e/specs/site-editor/pages.spec.js | 8 +- .../e2e/specs/widgets/editing-widgets.spec.js | 724 +++++++++++ .../blocks/core__gallery-with-caption.json | 1 + .../fixtures/blocks/core__gallery.json | 1 + .../blocks/core__gallery__columns.json | 1 + .../blocks/core__gallery__deprecated-7.json | 1 + .../blocks/core__post-featured-image.json | 3 +- .../fixtures/blocks/core__search.json | 1 - .../blocks/core__search__custom-text.json | 1 - test/native/setup.js | 6 + 693 files changed, 13558 insertions(+), 10102 deletions(-) create mode 100644 docs/getting-started/fundamentals/static-dynamic-rendering.md delete mode 100644 docs/how-to-guides/curating-the-editor-experience.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/README.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/block-locking.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/patterns.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/theme-json.md rename docs/how-to-guides/themes/{theme-json.md => global-settings-and-styles.md} (98%) delete mode 100644 lib/block-supports/behaviors.php delete mode 100644 lib/class-wp-theme-json-schema-gutenberg.php create mode 100644 lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php delete mode 100644 lib/experimental/interactivity-api/class-wp-interactivity-store.php create mode 100644 lib/experimental/interactivity-api/directives/wp-interactive.php create mode 100644 lib/experimental/interactivity-api/initial-state.php delete mode 100644 lib/experimental/interactivity-api/store.php create mode 100644 packages/block-editor/src/components/block-canvas/style.scss create mode 100644 packages/block-editor/src/components/block-list/use-scroll-upon-insertion.native.js rename packages/{edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js => block-editor/src/components/global-styles/get-global-styles-changes.js} (54%) rename packages/{edit-site/src/components/global-styles/screen-revisions/test/get-revision-changes.js => block-editor/src/components/global-styles/test/get-global-styles-changes.js} (67%) create mode 100644 packages/block-editor/src/components/media-upload-progress/constants.js delete mode 100644 packages/block-editor/src/components/offline-status/index.native.js create mode 100644 packages/components/src/mobile/image/constants.js rename packages/components/src/mobile/keyboard-aware-flat-list/test/{use-scroll-to-text-input.native.js => use-scroll-to-section.native.js} (71%) create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll.native.js create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-element.native.js rename packages/components/src/mobile/keyboard-aware-flat-list/{use-scroll-to-text-input.native.js => use-scroll-to-section.native.js} (56%) create mode 100644 packages/components/src/mobile/keyboard-aware-flat-list/use-scroll.native.js create mode 100644 packages/dataviews/src/utils.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/a.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/b.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/index.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dependency-graph/webpack.config.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/a.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/b.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/index.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/cyclic-dynamic-dependency-graph/webpack.config.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/module-renames/index.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/module-renames/webpack.config.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-interactivity/index.js create mode 100644 packages/dependency-extraction-webpack-plugin/test/fixtures/wordpress-interactivity/webpack.config.js delete mode 100644 packages/e2e-tests/specs/editor/various/__snapshots__/block-editor-keyboard-shortcuts.test.js.snap delete mode 100644 packages/e2e-tests/specs/editor/various/allowed-patterns.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/block-editor-keyboard-shortcuts.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/datepicker.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/dropdown-menu.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/editor-modes.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/invalid-block.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/nux.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/preferences.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/publish-panel.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/publishing.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/scheduling.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/sidebar.test.js delete mode 100644 packages/e2e-tests/specs/editor/various/taxonomies.test.js delete mode 100644 packages/e2e-tests/specs/experiments/experimental-features.js delete mode 100644 packages/e2e-tests/specs/experiments/fixtures/menu-items-request-fixture.json delete mode 100644 packages/e2e-tests/specs/widgets/editing-widgets.test.js delete mode 100644 packages/edit-post/src/components/secondary-sidebar/list-view-outline.js create mode 100644 packages/edit-post/src/hooks/use-post-history.js create mode 100644 packages/edit-site/src/components/block-editor/use-post-link-props.js rename packages/edit-site/src/components/global-styles/font-library-modal/{tab-layout.js => tab-panel-layout.js} (85%) delete mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js create mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js delete mode 100644 packages/edit-site/src/components/header-edit-mode/undo-redo/redo.js delete mode 100644 packages/edit-site/src/components/header-edit-mode/undo-redo/undo.js create mode 100644 packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js create mode 100644 packages/edit-site/src/components/page-patterns/dataviews-patterns.js create mode 100644 packages/edit-site/src/components/preferences-modal/enable-panel-option.js delete mode 100644 packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js delete mode 100644 packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js delete mode 100644 packages/edit-site/src/components/secondary-sidebar/style.scss create mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-pages-dataviews/index.js rename packages/{edit-post/src/components/header/header-toolbar => editor/src/components/document-tools}/index.js (66%) rename packages/{edit-post/src/components/header/header-toolbar => editor/src/components/document-tools}/style.scss (63%) create mode 100644 packages/editor/src/components/editor-canvas/style.scss rename packages/{edit-post/src/components/secondary-sidebar/inserter-sidebar.js => editor/src/components/inserter-sidebar/index.js} (81%) create mode 100644 packages/editor/src/components/inserter-sidebar/style.scss rename packages/{edit-post/src/components/secondary-sidebar/list-view-sidebar.js => editor/src/components/list-view-sidebar/index.js} (90%) create mode 100644 packages/editor/src/components/list-view-sidebar/list-view-outline.js rename packages/{edit-post/src/components/secondary-sidebar => editor/src/components/list-view-sidebar}/style.scss (59%) create mode 100644 packages/editor/src/components/offline-status/index.native.js rename packages/{block-editor => editor}/src/components/offline-status/style.native.scss (100%) create mode 100644 packages/editor/src/components/offline-status/test/index.native.js create mode 100644 packages/format-library/src/lock-unlock.js create mode 100644 packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js create mode 100644 packages/preferences-persistence/src/migrations/preferences-package-data/test/convert-editor-settings.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js delete mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js create mode 100644 packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js create mode 100644 packages/scripts/utils/block-json.js rename patches/{react-native+0.71.11.patch => react-native+0.71.11+001+initial.patch} (100%) create mode 100644 patches/react-native+0.71.11+002+boost-podspec.patch create mode 100644 phpunit/experimental/interactivity-api/class-wp-interactivity-initial-state-test.php delete mode 100644 phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php delete mode 100644 phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php create mode 100644 phpunit/tests/fonts/font-library/wpFontLibrary/base.php create mode 100644 phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php create mode 100644 test/e2e/specs/editor/various/allowed-patterns.spec.js create mode 100644 test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js rename packages/e2e-tests/specs/editor/various/core-settings.test.js => test/e2e/specs/editor/various/core-settings.spec.js (58%) create mode 100644 test/e2e/specs/editor/various/datepicker.spec.js create mode 100644 test/e2e/specs/editor/various/dropdown-menu.spec.js create mode 100644 test/e2e/specs/editor/various/editor-modes.spec.js create mode 100644 test/e2e/specs/editor/various/invalid-block.spec.js create mode 100644 test/e2e/specs/editor/various/nux.spec.js create mode 100644 test/e2e/specs/editor/various/preferences.spec.js create mode 100644 test/e2e/specs/editor/various/publishing.spec.js create mode 100644 test/e2e/specs/editor/various/scheduling.spec.js create mode 100644 test/e2e/specs/editor/various/sidebar.spec.js create mode 100644 test/e2e/specs/editor/various/taxonomies.spec.js create mode 100644 test/e2e/specs/widgets/editing-widgets.spec.js diff --git a/.eslintrc.js b/.eslintrc.js index 122ec45369c224..b7962fddda881a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -61,10 +61,6 @@ const restrictedImports = [ importNames: [ 'combineReducers' ], message: 'Please use `combineReducers` from `@wordpress/data` instead.', }, - { - name: 'puppeteer-testing-library', - message: '`puppeteer-testing-library` is still experimental.', - }, { name: '@emotion/css', message: diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cdeffc9b14c88a..76d1e45e3c6ce8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,3 +11,16 @@ updates: labels: - 'GitHub Actions' - '[Type] Build Tooling' + groups: + github-actions: + patterns: + - '*' + exclude-patterns: + - 'actions/setup-java' + - 'gradle/*' + - 'reactivecircus/*' + react-native: + patterns: + - 'actions/setup-java' + - 'gradle/*' + - 'reactivecircus/*' diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index a2628bf7af6160..5163a343541ca7 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -171,7 +171,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Node.js - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version-file: '.nvmrc' check-latest: true @@ -270,12 +270,12 @@ jobs: run: echo "version=$(echo $VERSION | cut -d / -f 3 | sed 's/-rc./ RC/' )" >> $GITHUB_OUTPUT - name: Download Plugin Zip Artifact - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 + uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 with: name: gutenberg-plugin - name: Download Release Notes Artifact - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 + uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 with: name: release-notes @@ -333,7 +333,7 @@ jobs: git config user.email gutenberg@wordpress.org - name: Setup Node.js - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 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 23b245cb6f114b..c7959b2992f0eb 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -43,7 +43,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Node.js - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version-file: '.nvmrc' check-latest: true diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index d065bf8afad44d..362aec73cd6241 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -120,7 +120,7 @@ jobs: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v4.1.0 id: download_artifact # Don't fail the job if there isn't any flaky tests report. continue-on-error: true diff --git a/.github/workflows/php-changes-detection.yml b/.github/workflows/php-changes-detection.yml index cd3c2664548fd4..fc0ba83b95dbc4 100644 --- a/.github/workflows/php-changes-detection.yml +++ b/.github/workflows/php-changes-detection.yml @@ -17,7 +17,7 @@ jobs: - name: Get changed PHP files id: changed-files-php - uses: tj-actions/changed-files@94549999469dbfa032becf298d95c87a14c34394 # v40.2.2 + uses: tj-actions/changed-files@716b1e13042866565e00e85fd4ec490e186c4a2f # v41.0.1 with: files: | *.{php} diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml index 163012451d6002..ad108f5db5b049 100644 --- a/.github/workflows/publish-npm-packages.yml +++ b/.github/workflows/publish-npm-packages.yml @@ -67,7 +67,7 @@ jobs: - name: Setup Node.js if: ${{ github.event.inputs.release_type != 'wp' }} - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version-file: 'cli/.nvmrc' registry-url: 'https://registry.npmjs.org' @@ -75,7 +75,7 @@ jobs: - name: Setup Node.js (for WP major version) if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 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 785b42a19054db..d0b8778a1d3c7c 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -18,7 +18,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Node.js - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version-file: '.nvmrc' check-latest: true diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index e1c51a62ed44e7..e0a3b9639cf389 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -28,7 +28,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Java - uses: actions/setup-java@0ab4596768b603586c0de567f2430c30f5b0d2b0 # v3.13.0 + uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0 with: distribution: 'corretto' java-version: '17' @@ -47,7 +47,7 @@ jobs: run: npm run native test:e2e:setup - name: Gradle cache - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a # v2.9.0 + uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1 - name: AVD cache uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 @@ -60,7 +60,7 @@ jobs: - name: Create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b # v2.28.0 + uses: reactivecircus/android-emulator-runner@99a4aac18b4df9b3af66c4a1f04c1f23fa10c270 # v2.29.0 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -71,7 +71,7 @@ jobs: script: echo "Generated AVD snapshot for caching." - name: Run tests - uses: reactivecircus/android-emulator-runner@d94c3fbe4fe6a29e4a5ba47c12fb47677c73656b # v2.28.0 + uses: reactivecircus/android-emulator-runner@99a4aac18b4df9b3af66c4a1f04c1f23fa10c270 # v2.29.0 with: api-level: ${{ matrix.api-level }} force-avd-creation: false diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index 2465f357a97cf0..e0a899908bbd46 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -27,7 +27,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Node.js - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1 with: node-version-file: '.nvmrc' check-latest: true diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index 02ba0e8cd50ff0..d6d5f8da1102bb 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -189,7 +189,7 @@ jobs: sed -i "s/$STABLE_TAG_PLACEHOLDER/Stable tag: $VERSION/g" ./trunk/readme.txt - name: Download Changelog Artifact - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 + uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 with: name: changelog trunk path: trunk @@ -247,7 +247,7 @@ jobs: sed -i "s/$STABLE_TAG_PLACEHOLDER/Stable tag: $VERSION/g" "$VERSION/readme.txt" - name: Download Changelog Artifact - uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 + uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 with: name: changelog trunk path: ${{ github.event.release.name }} diff --git a/LICENSE.md b/LICENSE.md index 1dcb4d22cba5e6..983294723c4806 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ ## Gutenberg - Copyright 2016-2023 by the contributors + Copyright 2016-2024 by the contributors **License for Contributions (on and after April 15, 2021)** diff --git a/README.md b/README.md index 5ba112319b405c..d5b299baadc7f8 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Get hands on: check out the [block editor live demo](https://wordpress.org/guten Extending and customizing is at the heart of the WordPress platform, this is no different for the Gutenberg project. The editor and future products can be extended by third-party developers using plugins. -Review the [Create a Block tutorial](/docs/getting-started/create-block/README.md) for the fastest way to get started extending the block editor. See the [Developer Documentation](https://developer.wordpress.org/block-editor/#develop-for-the-block-editor) for extensive tutorials, documentation, and API references. +Review the [Create a Block tutorial](/docs/getting-started/devenv/get-started-with-create-block.md) for the fastest way to get started extending the block editor. See the [Developer Documentation](https://developer.wordpress.org/block-editor/#develop-for-the-block-editor) for extensive tutorials, documentation, and API references. ### Contribute to Gutenberg diff --git a/changelog.txt b/changelog.txt index 2ca3d6277edb8b..9268dc7edd1fba 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,311 @@ == Changelog == -= 17.4.0-rc.1 = += 17.5.0-rc.1 = + + + +## Changelog + +### Enhancements + +#### Editor Unification +- Editor: Add the show most used blocks preference to the site editor. ([57637](https://github.com/WordPress/gutenberg/pull/57637)) +- Editor: Migrate and unify the panel preferences. ([57529](https://github.com/WordPress/gutenberg/pull/57529)) +- Editor: Unify context text cursor preference. ([57479](https://github.com/WordPress/gutenberg/pull/57479)) +- Editor: Unify list view open preference. ([57504](https://github.com/WordPress/gutenberg/pull/57504)) +- Editor: Unify right click override preference. ([57468](https://github.com/WordPress/gutenberg/pull/57468)) +- Editor: Unify show icon labels preference. ([57480](https://github.com/WordPress/gutenberg/pull/57480)) +- Editor: Unify spotlight mode preference. ([57533](https://github.com/WordPress/gutenberg/pull/57533)) +- Editor: Unify the distraction free preference. ([57590](https://github.com/WordPress/gutenberg/pull/57590)) +- Editor: Unify the show block breadcrumbs preference. ([57506](https://github.com/WordPress/gutenberg/pull/57506)) +- Editor: Unify the top toolbar preference. ([57531](https://github.com/WordPress/gutenberg/pull/57531)) + +#### Components +- Components: Replace `TabPanel` with `Tabs` in inline color picker. ([57292](https://github.com/WordPress/gutenberg/pull/57292) +- Add `compact` size variant to InputControl-based components. ([57398](https://github.com/WordPress/gutenberg/pull/57398)) +- BaseControl: Connect to context system. ([57408](https://github.com/WordPress/gutenberg/pull/57408)) +- Replace `TabPanel` with `Tabs` in the Style Book. ([57287](https://github.com/WordPress/gutenberg/pull/57287)) +- Tooltip: Improve tests. ([57345](https://github.com/WordPress/gutenberg/pull/57345)) +- Update @ariakit/react to v0.3.12 and @ariakit/test to v0.3.7. ([57547](https://github.com/WordPress/gutenberg/pull/57547)) + +#### Font Library +- Font Library: Remove "has_font_mime_type" function. ([57364](https://github.com/WordPress/gutenberg/pull/57364)) +- Font Library: Update font uninstall modal text. ([57368](https://github.com/WordPress/gutenberg/pull/57368)) +- Font Library: Add progress-bar while uploading font assets. ([57463](https://github.com/WordPress/gutenberg/pull/57463)) +- Font Library: Singularize install font families endpoint. ([57569](https://github.com/WordPress/gutenberg/pull/57569)) +- Font Library: Unregister font collection. ([54701](https://github.com/WordPress/gutenberg/pull/54701)) + +#### Site Editor +- Add Template Modal: Update scroll related layout. ([57617](https://github.com/WordPress/gutenberg/pull/57617)) +- Components: Replace `TabPanel` with `Tabs` in the Font Library `Modal`. ([57181](https://github.com/WordPress/gutenberg/pull/57181)) + +#### Interactivity API +- Implement `wp_initial_state()`. ([57556](https://github.com/WordPress/gutenberg/pull/57556)) +- Server directive processing: Stop processing non-interactive blocks. ([56302](https://github.com/WordPress/gutenberg/pull/56302)) +- Interactive template: Use viewModule. ([57712](https://github.com/WordPress/gutenberg/pull/57712)) +- Navigation Block: Use dom.focus for focus control. ([57362](https://github.com/WordPress/gutenberg/pull/57362)) + + +#### Site Editor +- Site editor: Add padding to entity save panel header. ([57471](https://github.com/WordPress/gutenberg/pull/57471)) +- Site editor: Add margin to entity save panel header via a classname. ([57473](https://github.com/WordPress/gutenberg/pull/57473)) + + +#### Block Library +- Post Featured Image: Add a useFirstImageFromPost attribute. ([56573](https://github.com/WordPress/gutenberg/pull/56573)) +- Gallery Block: Add random order setting. ([57477](https://github.com/WordPress/gutenberg/pull/57477)) +- Image Block: Change upload icon label. ([57704](https://github.com/WordPress/gutenberg/pull/57704)) + + +### Bug Fixes + +- Avoid using a memoized selector without dependencies. ([57257](https://github.com/WordPress/gutenberg/pull/57257)) +- Core Data: Pass the 'options' argument to data action shortcuts. ([57383](https://github.com/WordPress/gutenberg/pull/57383)) +- Preferences: Update accessibility scope to "core". ([57563](https://github.com/WordPress/gutenberg/pull/57563)) + +#### Block Editor +- Fix Link UI displaying out of sync results. ([57522](https://github.com/WordPress/gutenberg/pull/57522)) +- Give iframe fallback background color. ([57330](https://github.com/WordPress/gutenberg/pull/57330)) +- Rich text: Add HTML string methods to RichTextData. ([57322](https://github.com/WordPress/gutenberg/pull/57322)) + +#### Block Library +- Footnotes: Fix wrong link when adding more than 9 footnotes. ([57599](https://github.com/WordPress/gutenberg/pull/57599)) +- Table: Remove unnecessary margin override in editor styles. ([57699](https://github.com/WordPress/gutenberg/pull/57699)) +- Template Part block: Fix template part path arg missing from actions. ([56790](https://github.com/WordPress/gutenberg/pull/56790)) + +#### Components +- DuotonePicker: Fix top margin when no duotone options. ([57489](https://github.com/WordPress/gutenberg/pull/57489)) +- NavigatorProvider: Exclude size value from contain CSS rule. ([57498](https://github.com/WordPress/gutenberg/pull/57498)) +- Snackbar: Fix icon positioning. ([57377](https://github.com/WordPress/gutenberg/pull/57377)) + +#### Patterns +- Pattern Overrides: Add `template-lock: All` to pattern inner blocks to prevent deletion/insertion. ([57661](https://github.com/WordPress/gutenberg/pull/57661)) +- Refactor the findOrCreate term method. ([57655](https://github.com/WordPress/gutenberg/pull/57655)) +- Edit source pattern in focus mode in post and site editors. ([57036](https://github.com/WordPress/gutenberg/pull/57036)) + + +#### Site Editor +- Make sure comamnd palette toggle does not disappear while being clicked. ([57420](https://github.com/WordPress/gutenberg/pull/57420)) +- Reinstate iframe CSS for editor canvas container. ([57503](https://github.com/WordPress/gutenberg/pull/57503)) + +#### Global Styles +- Use `is-layout` pattern on layout generated classname. ([57564](https://github.com/WordPress/gutenberg/pull/57564)) +- Global styles revisions: Add individual headings translations, update tests. ([57472](https://github.com/WordPress/gutenberg/pull/57472)) +- Global style revisions: Move change summary code and tests to block editor package. ([57411](https://github.com/WordPress/gutenberg/pull/57411)) +- Reduce specificity of block style variation selectors. ([57659](https://github.com/WordPress/gutenberg/pull/57659)) +- Background image block support: Add tests for size and repeat output. ([57474](https://github.com/WordPress/gutenberg/pull/57474)) + + +#### Post Editor +- Fix Template preview menu item accessibility. ([57456](https://github.com/WordPress/gutenberg/pull/57456)) +- Fullscreen mode description: Use full text instead of abbreviation. ([57518](https://github.com/WordPress/gutenberg/pull/57518)) +- Improve pre-publish checks naming consistency. ([57019](https://github.com/WordPress/gutenberg/pull/57019)) +- Make the Replace featured image button perceivable by assistive technologies. ([57453](https://github.com/WordPress/gutenberg/pull/57453)) + +#### Components +- Label the HeightControl. ([57683](https://github.com/WordPress/gutenberg/pull/57683)) +- NumberControl: Make increment and decrement buttons keyboard accessible. ([57402](https://github.com/WordPress/gutenberg/pull/57402)) + +#### Block Tools +- Update the position of the patterns tab in the inserter menu. ([55688](https://github.com/WordPress/gutenberg/pull/55688)) +- Use full text instead of abbreviation for min height setting. ([57680](https://github.com/WordPress/gutenberg/pull/57680)) +- ResizableEditor: Fix tab order for resize handles. ([57475](https://github.com/WordPress/gutenberg/pull/57475)) +- Keep Lock button it in the toolbar until unmounted. ([57229](https://github.com/WordPress/gutenberg/pull/57229)) +- Custom field connections: Better description on Experiments page. ([57501](https://github.com/WordPress/gutenberg/pull/57501)) + +### Performance + +#### Block Library +- File: Remove 'block-editor' store subscription. ([57511](https://github.com/WordPress/gutenberg/pull/57511)) +- Remove store subscriptions from Audio and Video blocks. ([57449](https://github.com/WordPress/gutenberg/pull/57449)) +- Site Logo: Remove unnecessary 'block-editor' store subscription. ([57513](https://github.com/WordPress/gutenberg/pull/57513)) +- Send numerical post id when uploading image. ([57388](https://github.com/WordPress/gutenberg/pull/57388)) +- PostFeaturedImage: Remove unnecessary 'block-editor' store subscription. ([57554](https://github.com/WordPress/gutenberg/pull/57554)) + +### Experiments + +#### Data Views +- DataViews: Use DropdownMenuRadioItem component when possible. ([57505](https://github.com/WordPress/gutenberg/pull/57505)) +- Align icon size + placement in Patterns data view. ([57548](https://github.com/WordPress/gutenberg/pull/57548)) +- DataViews: Add `duplicate pattern` action in patterns page. ([57592](https://github.com/WordPress/gutenberg/pull/57592)) +- DataViews: Add duplicate template pattern action. ([57638](https://github.com/WordPress/gutenberg/pull/57638)) +- DataViews: Add footer to Pages sidebar. ([57690](https://github.com/WordPress/gutenberg/pull/57690)) +- DataViews: Add new page button in `Pages`. ([57685](https://github.com/WordPress/gutenberg/pull/57685)) +- DataViews: Add sync filter in patterns page. ([57532](https://github.com/WordPress/gutenberg/pull/57532)) +- DataViews: Consolidate CSS selectors naming schema. ([57651](https://github.com/WordPress/gutenberg/pull/57651)) +- DataViews: Fallback to `(no title)` is there's no rendered title. ([57434](https://github.com/WordPress/gutenberg/pull/57434)) +- DataViews: Hide actions menu upon selecting a layout. ([57418](https://github.com/WordPress/gutenberg/pull/57418)) +- DataViews: Make `fields` dependant on `view.type`. ([57450](https://github.com/WordPress/gutenberg/pull/57450)) +- DataViews: Memoize `onSetSelection`. ([57458](https://github.com/WordPress/gutenberg/pull/57458)) +- DataViews: Prevent unnecessary re-renders of Pagination. ([57454](https://github.com/WordPress/gutenberg/pull/57454)) +- DataViews: Prevent unnecessary re-renders. ([57452](https://github.com/WordPress/gutenberg/pull/57452)) +- DataViews: Update names for `DropdownMenuRadioItemCustom`. ([57416](https://github.com/WordPress/gutenberg/pull/57416)) +- DataViews: Use i18n._x to clarify term "Duplicate". ([57686](https://github.com/WordPress/gutenberg/pull/57686)) +- DataViews: Use in patterns page. ([57333](https://github.com/WordPress/gutenberg/pull/57333)) +- Dataview: Change the stacking order of table header. ([57565](https://github.com/WordPress/gutenberg/pull/57565)) +- Dataviews: Add some client side data handling utils. ([57488](https://github.com/WordPress/gutenberg/pull/57488)) +- Make title display in grid views consistent. ([57553](https://github.com/WordPress/gutenberg/pull/57553)) +- Update Table layout design details. ([57644](https://github.com/WordPress/gutenberg/pull/57644)) +- Update pagination spacing in List layout. ([57670](https://github.com/WordPress/gutenberg/pull/57670)) +- Update table header gap. ([57671](https://github.com/WordPress/gutenberg/pull/57671)) +- [Dataviews] Table layout: Ensure focus is not lost on interaction. ([57340](https://github.com/WordPress/gutenberg/pull/57340)) + +#### Patterns +- [Pattern Overrides] Fix duplication of inner blocks. ([57538](https://github.com/WordPress/gutenberg/pull/57538)) +- [Pattern overrides] Allow multiple attributes overrides. ([57573](https://github.com/WordPress/gutenberg/pull/57573)) + + +### Documentation + +- Add links to additional local dev tools in Block Developement Environment readme. ([57682](https://github.com/WordPress/gutenberg/pull/57682)) +- Add new section to the Quick Start Guide about wp-env. ([57559](https://github.com/WordPress/gutenberg/pull/57559)) +- Block JSON schema: Add renaming key to supports definition. ([57373](https://github.com/WordPress/gutenberg/pull/57373)) +- Break out the Curating the Editor Experience doc into its own How-to Guides section. ([57289](https://github.com/WordPress/gutenberg/pull/57289)) +- Change the slug for the theme.json doc to avoid conflicts. ([57410](https://github.com/WordPress/gutenberg/pull/57410)) +- Docs/tutorial: Fix opposite condition for content generation in render.php. ([57445](https://github.com/WordPress/gutenberg/pull/57445)) +- Docs: Fundamentals of Block Development - Static or Dynamic rendering of a block. ([57250](https://github.com/WordPress/gutenberg/pull/57250)) +- Docs: Update sample code to fix React warning error on Tutorial page. ([57412](https://github.com/WordPress/gutenberg/pull/57412)) +- Fix formatting issue due to incorrect link parsing in the Quick Start Guide. ([57693](https://github.com/WordPress/gutenberg/pull/57693)) +- Fix incorrect heading level in Editor curation documentation. ([57409](https://github.com/WordPress/gutenberg/pull/57409)) +- Fix two typos in tutorial.md. ([57627](https://github.com/WordPress/gutenberg/pull/57627)) +- Fix: Create block getting started links. ([57551](https://github.com/WordPress/gutenberg/pull/57551)) +- Improve the static vs dynamic rendering comment in the block tutorial. ([57284](https://github.com/WordPress/gutenberg/pull/57284)) +- Update copyright year to 2024 in `license.md`. ([57481](https://github.com/WordPress/gutenberg/pull/57481)) +- Update the "Build your first block" tutorial based on user feedback. ([57403](https://github.com/WordPress/gutenberg/pull/57403)) +- Update: Material design icons link. ([57550](https://github.com/WordPress/gutenberg/pull/57550)) + + +### Code Quality +- Editor: Unify the DocumentTools component. ([57214](https://github.com/WordPress/gutenberg/pull/57214)) +- Make getLastFocus and setLastFocus private. ([57612](https://github.com/WordPress/gutenberg/pull/57612)) +- Remove deprecated `behaviors` syntax. ([57165](https://github.com/WordPress/gutenberg/pull/57165)) +- Avoid extra `useMarkPersistent` dispatch calls. ([57435](https://github.com/WordPress/gutenberg/pull/57435)) +- Clean up code editor CSS. ([57519](https://github.com/WordPress/gutenberg/pull/57519)) +- Combine selectors in 'useTransformCommands'. ([57424](https://github.com/WordPress/gutenberg/pull/57424)) + +#### Block Library +- Background image: Add has-background classname when background image is applied. ([57495](https://github.com/WordPress/gutenberg/pull/57495)) +- File: Remove unnecessary synchronization effect. ([57585](https://github.com/WordPress/gutenberg/pull/57585)) +- Navigation: Refactor mobile overlay breakpoints to JS. ([57520](https://github.com/WordPress/gutenberg/pull/57520)) +- Search Block: Remove unused `buttonBehavior` attribute. ([53467](https://github.com/WordPress/gutenberg/pull/53467)) + +#### Patterns +- Improve inserter pattern constants. ([57570](https://github.com/WordPress/gutenberg/pull/57570)) +- Remove duplicate setting for `getPostLinkProps` and prefer stable naming. ([57535](https://github.com/WordPress/gutenberg/pull/57535)) +- Rename `patternBlock` to `patternPost`. ([57568](https://github.com/WordPress/gutenberg/pull/57568)) + +#### Post Editor +- Editor: Use hooks instead of HoCs in 'PostVisibilityCheck'. ([57705](https://github.com/WordPress/gutenberg/pull/57705)) +- Quality: Avoid React warning when changing rendering mode. ([57413](https://github.com/WordPress/gutenberg/pull/57413)) + +#### Block Editor +- Editor: Unify the inserter sidebar. ([57466](https://github.com/WordPress/gutenberg/pull/57466)) +- Remove unused parameters from useOnBlockDrop. ([57527](https://github.com/WordPress/gutenberg/pull/57527)) + +#### List View +- Editor: Unify the list view sidebar between the post and site editors. ([57467](https://github.com/WordPress/gutenberg/pull/57467)) +- Add drag cursor to draggable list items. ([57493](https://github.com/WordPress/gutenberg/pull/57493)) + +### Tools + +- Dependency Extraction Webpack Plugin: Use `import` for module externals. ([57577](https://github.com/WordPress/gutenberg/pull/57577)) +- DependencyExtractionWebpackPlugin: Add true shorthand for requestToExternalModule. ([57593](https://github.com/WordPress/gutenberg/pull/57593)) +- DependencyExtractionWebpackPlugin: Use module for @wordpress/interactivity. ([57602](https://github.com/WordPress/gutenberg/pull/57602)) +- Fix webpack not setting environment.module true. ([57714](https://github.com/WordPress/gutenberg/pull/57714)) +- Modules: Load the import map polyfill when needed. ([57256](https://github.com/WordPress/gutenberg/pull/57256)) +- Blocks: Add handling for block.json viewModule. ([57437](https://github.com/WordPress/gutenberg/pull/57437)) + +#### Testing +- Allowed Patterns end-to-end test - move tests that run with a subset of allowed blocks into a group. ([57496](https://github.com/WordPress/gutenberg/pull/57496)) +- Clean up end-to-end tests package. ([57575](https://github.com/WordPress/gutenberg/pull/57575)) +- Fix flaky 'Post publish button' end-to-end test. ([57407](https://github.com/WordPress/gutenberg/pull/57407)) +- Migrate 'allowed patterns' end-to-end tests to Playwright. ([57399](https://github.com/WordPress/gutenberg/pull/57399)) +- Migrate 'block editor keyboard shortcuts' end-to-end tests to Playwright. ([57422](https://github.com/WordPress/gutenberg/pull/57422)) +- Migrate 'core settings' end-to-end tests to Playwright. ([57581](https://github.com/WordPress/gutenberg/pull/57581)) +- Migrate 'datepicker' end-to-end tests to Playwright. ([57545](https://github.com/WordPress/gutenberg/pull/57545)) +- Migrate 'dropdown menu' end-to-end tests to Playwright. ([57663](https://github.com/WordPress/gutenberg/pull/57663)) +- Migrate 'editor modes' end-to-end tests to Playwright. ([57574](https://github.com/WordPress/gutenberg/pull/57574)) +- Migrate 'invalid blocks' end-to-end tests to Playwright. ([57508](https://github.com/WordPress/gutenberg/pull/57508)) +- Migrate 'nux' end-to-end tests to Playwright. ([57542](https://github.com/WordPress/gutenberg/pull/57542)) +- Migrate 'preferences' end-to-end tests to Playwright. ([57446](https://github.com/WordPress/gutenberg/pull/57446)) +- Migrate 'publishing' end-to-end tests to Playwright. ([57521](https://github.com/WordPress/gutenberg/pull/57521)) +- Migrate 'scheduling' end-to-end tests to Playwright. ([57539](https://github.com/WordPress/gutenberg/pull/57539)) +- Migrate 'sidebar' end-to-end tests to Playwright. ([57448](https://github.com/WordPress/gutenberg/pull/57448)) +- Migrate 'taxonomies' end-to-end tests to Playwright. ([57662](https://github.com/WordPress/gutenberg/pull/57662)) +- Migrate `editing-widgets` to Playwright. ([57483](https://github.com/WordPress/gutenberg/pull/57483)) +- Migrate remaining 'publish panel' end-to-end tests to Playwright. ([57432](https://github.com/WordPress/gutenberg/pull/57432)) +- Update 'missing block' end-to-end tests to use the 'setContent' helper. ([57509](https://github.com/WordPress/gutenberg/pull/57509)) + +#### Build Tooling +- Group GitHub Action Dependabot updates. ([57591](https://github.com/WordPress/gutenberg/pull/57591)) +- WP Scripts: Build block.json viewModule. ([57461](https://github.com/WordPress/gutenberg/pull/57461)) +- Dependency Extraction Webpack Plugin: Add Module support. ([57199](https://github.com/WordPress/gutenberg/pull/57199)) + +## First time contributors + +The following PRs were merged by first time contributors: + +- @HrithikDalal: Font Library: Update font uninstall modal text. ([57368](https://github.com/WordPress/gutenberg/pull/57368)) +- @muhme: Fix two typos in tutorial.md. ([57627](https://github.com/WordPress/gutenberg/pull/57627)) + + +## Contributors + +The following contributors merged PRs in this release: + +@afercia @andrewhayward @andrewserong @atachibana @c4rl0sbr4v0 @carolinan @chad1008 @ciampo @DAreRodz @dcalhoun @derekblank @desrosj @ellatrix @fai-sal @fluiddot @geriux @getdave @glendaviesnz @gziolo @hbhalodia @HrithikDalal @jameskoster @jeryj @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @Mamaduka @matiasbenedetto @mcsf @michalczaplinski @mirka @muhme @ndiego @ntsekouras @oandregal @ockham @ramonjd @scruffian @sirreal @Soean @t-hamano @talldan @tellthemachines @youknowriad + + += 17.3.2 = + +## Changelog + +### Bug Fixes + +- Site editor: fix image upload bug ([57040](https://github.com/WordPress/gutenberg/pull/57040)) + +## Contributors + +The following contributors merged PRs in this release: + +@glendaviesnz + + + + += 17.4.1 = + +## Changelog + +### Bug Fixes + +- Rich text: add HTML string methods to RichTextData ([57322](https://github.com/WordPress/gutenberg/pull/57322)) + +## Contributors + +The following contributors merged PRs in this release: + +@ellatrix + + += 17.3.1 = + +## Changelog + +### Bug Fixes + +- Rich text: add HTML string methods to RichTextData ([57322](https://github.com/WordPress/gutenberg/pull/57322)) + +## Contributors + +The following contributors merged PRs in this release: + +@ellatrix + + + += 17.4.0 = + ## Changelog @@ -25,6 +330,7 @@ - Add the featured image panel. ([57053](https://github.com/WordPress/gutenberg/pull/57053)) - Editor: Unify revision panel between post and site editors. ([57010](https://github.com/WordPress/gutenberg/pull/57010)) - Social Link block: Obfuscate email address. ([57384](https://github.com/WordPress/gutenberg/pull/57384)) +- DataViews: Mark the new Templates pages as stable. ([57109](https://github.com/WordPress/gutenberg/pull/57109)) #### Design Tools - Background image: Add backgroundSize and repeat features. ([57005](https://github.com/WordPress/gutenberg/pull/57005)) @@ -149,7 +455,6 @@ - DataViews: Make `deferredRendering` prop optional. ([57334](https://github.com/WordPress/gutenberg/pull/57334)) - DataViews: Make `getItemId` optional. ([57308](https://github.com/WordPress/gutenberg/pull/57308)) - DataViews: Make filters footprint more condensed. ([56983](https://github.com/WordPress/gutenberg/pull/56983)) -- DataViews: Mark the new Templates pages as stable. ([57109](https://github.com/WordPress/gutenberg/pull/57109)) - DataViews: Memoize `onSelectionChange` callback. ([57390](https://github.com/WordPress/gutenberg/pull/57390)) - DataViews: Remove `paginationInfo` prop from ViewComponent. ([57306](https://github.com/WordPress/gutenberg/pull/57306)) - DataViews: Remove reference to edit site class. ([57075](https://github.com/WordPress/gutenberg/pull/57075)) @@ -298,6 +603,8 @@ The following contributors merged PRs in this release: @andrewhayward @andrewserong @anton-vlasenko @arthur791004 @atachibana @bacoords @BenjaminZekavica @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @colorful-tones @DAreRodz @dcalhoun @ddryo @derekblank @desrosj @dotslashbit @ellatrix @fluiddot @geriux @getdave @glendaviesnz @gvgvgvijayan @jameskoster @jasmussen @jeryj @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @koen12344 @kurudrive @luisherranz @Mamaduka @matiasbenedetto @mcsf @mhimon @mirka @ndiego @ntsekouras @oandregal @ockham @ramonjd @richtabor @SiobhyB @sirreal @stokesman @t-hamano @tellthemachines @TimBroddin @youknowriad @yuliyan + + = 17.2.4 = ## Changelog diff --git a/docs/contributors/documentation/README.md b/docs/contributors/documentation/README.md index 7089bf57141687..454f455ba891db 100644 --- a/docs/contributors/documentation/README.md +++ b/docs/contributors/documentation/README.md @@ -31,7 +31,7 @@ See [the Git Workflow](/docs/contributors/code/git-workflow.md) documentation fo 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). +- **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/devenv/get-started-with-create-block.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. @@ -164,7 +164,7 @@ This is a **warning** callout. Note: In callout notices, links also need to be HTML `<a href></a>` notations. The usual link transformation is not applied to links in callouts. For instance, to reach the Getting started > Create Block page the URL in GitHub is -https://developer.wordpress.org/docs/getting-started/create-block/README.md +https://developer.wordpress.org/docs/getting-started/devenv/get-started-with-create-block.md and will have to be hardcoded for the endpoint in the Block Editor Handbook as https://developer.wordpress.org/block-editor/getting-started/create-block/ to link correctly in the handbook. diff --git a/docs/explanations/architecture/key-concepts.md b/docs/explanations/architecture/key-concepts.md index 1ba009f7823140..30318d29422d37 100644 --- a/docs/explanations/architecture/key-concepts.md +++ b/docs/explanations/architecture/key-concepts.md @@ -39,7 +39,7 @@ Given a block type, a block variation is a predefined set of its initial attribu **More on blocks** - **[Block API](/docs/reference-guides/block-api/README.md)** -- **[Tutorial: Building A Custom Block](/docs/getting-started/create-block/README.md)** +- **[Tutorial: Building A Custom Block](/docs/getting-started/devenv/get-started-with-create-block.md)** ## Reusable blocks @@ -65,6 +65,6 @@ More on [Site editing templates](/docs/explanations/architecture/full-site-editi ## Styles -Styles, formerly known as Global Styles and as such referenced in the code, is both an interface that users access through the editor and a configuration system done through [a `theme.json` file](/docs/how-to-guides/themes/theme-json.md). This file absorbs most of the configuration aspects usually scattered through various `add_theme_support` calls to simplify communicating with the editor. It thus aims to improve declaring what settings should be enabled, what specific tools a theme offers (like a custom color palette), the available design tools present, and an infrastructure that allows to coordinate the styles coming from WordPress, the active theme, and the user. +Styles, formerly known as Global Styles and as such referenced in the code, is both an interface that users access through the editor and a configuration system done through [a `theme.json` file](/docs/how-to-guides/themes/global-settings-and-styles.md). This file absorbs most of the configuration aspects usually scattered through various `add_theme_support` calls to simplify communicating with the editor. It thus aims to improve declaring what settings should be enabled, what specific tools a theme offers (like a custom color palette), the available design tools present, and an infrastructure that allows to coordinate the styles coming from WordPress, the active theme, and the user. Learn more about [Global Styles](/docs/explanations/architecture/styles.md#global-styles). diff --git a/docs/explanations/architecture/styles.md b/docs/explanations/architecture/styles.md index d62171a0622055..94a8e91f94edbe 100644 --- a/docs/explanations/architecture/styles.md +++ b/docs/explanations/architecture/styles.md @@ -15,7 +15,7 @@ The final HTML document is the result of a few things: The stylesheets loaded in the front end include: - **Blocks**. The stylesheets that come with the block. In the front end, you can find a single stylesheet with all block styles defined by WordPress (`wp-block-library-*` ) or separate stylesheets per block in use (as in `wp-block-group-*`, `wp-block-columns-*`, etc). See [this note](https://make.wordpress.org/core/2021/07/01/block-styles-loading-enhancements-in-wordpress-5-8/) for the full details. -- **Global styles**. These styles are generated on the fly by using data coming from a theme.json file: see [note](https://make.wordpress.org/core/2021/06/25/introducing-theme-json-in-wordpress-5-8/), [reference](https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/), and [how to guide](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-json/). Specifically, it merges the contents of the theme.json from WordPress, the theme.json from the theme (if it has one), and the user data provided via the global styles sidebar in the site editor. The result of processing this data is an embedded stylesheet whose id is `global-styles-inline-css`. +- **Global styles**. These styles are generated on the fly by using data coming from a theme.json file: see [note](https://make.wordpress.org/core/2021/06/25/introducing-theme-json-in-wordpress-5-8/), [reference](https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/), and [how to guide](https://developer.wordpress.org/block-editor/how-to-guides/themes/global-settings-and-styles/). Specifically, it merges the contents of the theme.json from WordPress, the theme.json from the theme (if it has one), and the user data provided via the global styles sidebar in the site editor. The result of processing this data is an embedded stylesheet whose id is `global-styles-inline-css`. - **Theme**. Historically, themes have enqueued their own stylesheets, where the id is based on the theme name, as in `twentytwentytwo-style-css`. In addition to having their own stylesheets they can now declare a theme.json file containing styles that will be part of the stylesheet generated by global styles. - **User**. Some of the user actions in the editor will generate style content. This is the case for features such as duotone, layout, or link color. - **Other**. WordPress and plugins can also enqueue stylesheets. @@ -515,7 +515,7 @@ There are currently four layout types in use: - Flex: Items are displayed using a Flexbox layout. Defaults to a horizontal orientation. Spacing between children is handled via the `gap` CSS property. - Grid: Items are displayed using a Grid layout. Defaults to an `auto-fill` approach to column generation but can also be set to a fixed number of columns. Spacing between children is handled via the `gap` CSS property. -For controlling spacing between blocks, and enabling block spacing controls see: [What is blockGap and how can I use it?](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-json/#what-is-blockgap-and-how-can-i-use-it). +For controlling spacing between blocks, and enabling block spacing controls see: [What is blockGap and how can I use it?](https://developer.wordpress.org/block-editor/how-to-guides/themes/global-settings-and-styles/#what-is-blockgap-and-how-can-i-use-it). ### Targeting layout or container blocks from themes diff --git a/docs/explanations/user-interface/block-design.md b/docs/explanations/user-interface/block-design.md index e3a7b84bfa583e..66411744fa5663 100644 --- a/docs/explanations/user-interface/block-design.md +++ b/docs/explanations/user-interface/block-design.md @@ -74,7 +74,7 @@ When referring to a block in documentation or UI, use title case for the block t - Latest Posts block - Media & Text block -Blocks should have an identifying icon, ideally using a single color. Try to avoid using the same icon used by an existing block. The core block icons are based on [Material Design Icons](https://material.io/tools/icons/). Look to that icon set, or to [Dashicons](https://developer.wordpress.org/resource/dashicons/) for style inspiration. +Blocks should have an identifying icon, ideally using a single color. Try to avoid using the same icon used by an existing block. The core block icons are based on [Material Design Icons](https://fonts.google.com/icons). Look to that icon set, or to [Dashicons](https://developer.wordpress.org/resource/dashicons/) for style inspiration. ![A screenshot of the block library with concise block names](https://developer.wordpress.org/files/2022/01/blocks-do.png) **Do:** diff --git a/docs/getting-started/devenv/README.md b/docs/getting-started/devenv/README.md index c891490437d431..47113c84d78dac 100644 --- a/docs/getting-started/devenv/README.md +++ b/docs/getting-started/devenv/README.md @@ -48,3 +48,10 @@ Refer to the [Get started with `wp-env`](/docs/getting-started/devenv/get-starte
Throughout the Handbook, you may also see references to wp-now. This is a lightweight tool powered by WordPress Playground that streamlines setting up a simple local WordPress environment. While still experimental, this tool is great for quickly testing WordPress releases, plugins, and themes.
+ +This list is not exhaustive, but here are several additional options to choose from if you prefer not to use `wp-env`: + +- [Local](https://localwp.com/) +- [XAMPP](https://www.apachefriends.org/) +- [MAMP](https://www.mamp.info/en/mamp/mac/) +- [Varying Vagrant Vagrants](https://varyingvagrantvagrants.org/) (VVV) diff --git a/docs/getting-started/fundamentals/README.md b/docs/getting-started/fundamentals/README.md index 799ff89aa39419..0683e55d7edf76 100644 --- a/docs/getting-started/fundamentals/README.md +++ b/docs/getting-started/fundamentals/README.md @@ -9,5 +9,6 @@ In this section, you will learn: 1. [**Registration of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How a block is registered in both the server and the client. 1. [**Block wrapper**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper) - How to set proper attributes to the block's markup wrapper. 1. [**The block in the Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-in-the-editor) - The block as a React component loaded in the Block Editor and its possibilities. -1. [**Markup representation of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block) - How blocks are represented in the DB or in templates. +1. [**Markup representation of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block) - How blocks are represented in the database, theme templates, or patterns. +1. [**Static or Dynamic rendering of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/static-dynamic-rendering) - How blocks can generate their output for the front end dynamically or statically. 1. [**Javascript in the Block Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor) - How to work with Javascript for the Block Editor. \ No newline at end of file diff --git a/docs/getting-started/fundamentals/markup-representation-block.md b/docs/getting-started/fundamentals/markup-representation-block.md index 20289f8f228ce8..506d0feb8d3d17 100644 --- a/docs/getting-started/fundamentals/markup-representation-block.md +++ b/docs/getting-started/fundamentals/markup-representation-block.md @@ -23,7 +23,7 @@ The [markup representation of a block is parsed for the Block Editor](https://de Whenever a block is saved, the `save` function, defined when the [block is registered in the client](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-javascript-client-side), is called to return the markup that will be saved into the database within the block delimiter's comment. If `save` is `null` (common case for blocks with dynamic rendering), only a single line block delimiter's comment is stored, along with any attributes The Post Editor checks that the markup created by the `save` function is identical to the block's markup saved to the database: -- If there are any differences, the Post Editor trigger a **block validation error**. +- If there are any differences, the Post Editor triggers a [block validation error](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#validation). - Block validation errors usually happen when a block’s `save` function is updated to change the markup produced by the block. - A block developer can mitigate these issues by adding a [**block deprecation**](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-deprecation/) to register the change in the block. diff --git a/docs/getting-started/fundamentals/static-dynamic-rendering.md b/docs/getting-started/fundamentals/static-dynamic-rendering.md new file mode 100644 index 00000000000000..34d5432850c45e --- /dev/null +++ b/docs/getting-started/fundamentals/static-dynamic-rendering.md @@ -0,0 +1,170 @@ +# Static or Dynamic rendering of a block + +The block's markup returned on the front end can be dynamically generated on the server when the block is requested from the client (dynamic blocks) or statically generated when the block is saved in the Block Editor (static blocks). + +
+The post Static vs. dynamic blocks: What’s the difference? provides a great introduction to static and dynamic blocks. +
+ +## Static rendering + +![Blocks with static rendering diagram](https://developer.wordpress.org/files/2024/01/static-rendering.png) + +Blocks are considered "static" when they have "static rendering", this is when their output for the front end is statically generated when saved to the database, as returned by their `save` functions. + +Blocks have static rendering **when no dynamic rendering method has been defined (or is available) for the block**. In this case, the output for the front end will be taken from the [markup representation of the block in the database](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block/) that is returned by its `save` function when the block is saved in the Block Editor. This type is block is often called a "static block". + +### How to define static rendering for a block + +The `save` function, which can be defined when [registering a block on the client](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-javascript-client-side), determines the markup of the block that will be stored in the database when the content is saved and eventually returned to the front end when there's a request. This markup is stored wrapped up in [unique block delimiters](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block/) but only the markup inside these block indicators is returned as the markup to be rendered for the block on the front end. + +To define static rendering for a block we define a `save` function for the block without any dynamic rendering method. + +
Example of static rendering of the preformatted core block +
+For example, the following save function of the preformatted core block... + +```js +import { RichText, useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + const { content } = attributes; + + return ( +
+			
+		
+ ); +} +``` + +...generates the following markup representation of the block when `attributes.content` has the value `"This is some preformatted text"`... + +```html + +
This is some preformatted text
+ +``` + +...and it will return the following markup for the block to the front end when there's a request. + +```html +
This is some preformatted text
+``` + +
+ +
+ +Blocks with dynamic rendering can also define a markup representation of the block (via the `save` function) which can be processed in the server before returning the markup to the front end. If no dynamic rendering method is found, any markup representation of the block in the database will be returned to the front end. + +
+The markup stored for a block can be modified before it gets rendered on the front end via hooks such as render_block or via $render_callback. +
+ +Some examples of core blocks with static rendering are: +- [`separator`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/separator) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/separator/save.js) function) +- [`spacer`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/spacer) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/spacer/save.js) function). +- [`button`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/button) (see its [`save`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/button/save.js) function). + + +## Dynamic rendering + +Blocks with dynamic rendering are blocks that **build their structure and content on the fly when the block is requested from the client**. This type of block is often called a "dynamic block". + +![Blocks with dynamic rendering diagram](https://developer.wordpress.org/files/2024/01/dynamic-rendering.png) + +There are some common use cases for dynamic blocks: + +1. **Blocks where content should change even if a post has not been updated**. An example is the [`latest-posts` core block](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/latest-posts), which will update its content on request time, everywhere it is used after a new post is published. +2. **Blocks where updates to the markup should be immediately shown on the front end of the website**. For example, if you update the structure of a block by adding a new class, adding an HTML element, or changing the layout in any other way, using a dynamic block ensures those changes are applied immediately on all occurrences of that block across the site. If a dynamic block is not used then when block code is updated, Gutenberg's [validation process](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#validation) generally applies, causing users to see the validation message: "This block appears to have been modified externally". + +### How to define dynamic rendering for a block + +A block can define dynamic rendering in two main ways: +1. Via the `render_callback` argument that can be passed to the [`register_block_type()` function](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-php-server-side). +1. Via a separate PHP file (usually named `render.php`) which path can be defined at the [`render` property of the `block.json`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#files-for-the-blocks-behavior-output-or-style). + +Both of these ways to define the block's dynamic rendering receive the following data: + - `$attributes` - The array of attributes for this block. + - `$content` - Rendered block output (markup of the block as stored in the database). + - `$block` - The instance of the [WP_Block](https://developer.wordpress.org/reference/classes/wp_block/) class that represents the block being rendered ([metadata of the block](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/)). + +
Example of dynamic rendering of the site-title core block +
+ +For example, the [`site-title`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/site-title) core block with the following function registered as [`render_callback`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/site-title/index.php)... + + +```php +function render_block_core_site_title( $attributes ) { + $site_title = get_bloginfo( 'name' ); + if ( ! $site_title ) { + return; + } + + $tag_name = 'h1'; + $classes = empty( $attributes['textAlign'] ) ? '' : "has-text-align-{$attributes['textAlign']}"; + if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) { + $classes .= ' has-link-color'; + } + + if ( isset( $attributes['level'] ) ) { + $tag_name = 0 === $attributes['level'] ? 'p' : 'h' . (int) $attributes['level']; + } + + if ( $attributes['isLink'] ) { + $aria_current = is_home() || ( is_front_page() && 'page' === get_option( 'show_on_front' ) ) ? ' aria-current="page"' : ''; + $link_target = ! empty( $attributes['linkTarget'] ) ? $attributes['linkTarget'] : '_self'; + + $site_title = sprintf( + '%4$s', + esc_url( home_url() ), + esc_attr( $link_target ), + $aria_current, + esc_html( $site_title ) + ); + } + $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( $classes ) ) ); + + return sprintf( + '<%1$s %2$s>%3$s', + $tag_name, + $wrapper_attributes, + // already pre-escaped if it is a link. + $attributes['isLink'] ? $site_title : esc_html( $site_title ) + ); +} +``` + +... generates the following markup representation of the block in the database (as [there's no `save` function defined for this block](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/site-title/index.js))... + +```html + +``` + +...and it could generate the following markup for the block to the front end when there's a request (depending on the specific values on the server at request time). + +``` +

My WordPress Website

+``` + +
+
+ +### HTML representation of dynamic blocks in the database (`save`) + +For dynamic blocks, the `save` callback function can return just `null`, which tells the editor to save only the block delimiter comment (along with any existing [block attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/)) to the database. These attributes are then passed into the server-side rendering callback, which will determine how to display the block on the front end of your site. **When `save` is `null`, the Block Editor will skip the [block markup validation process](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#validation)**, avoiding issues with frequently changing markup. + +Blocks with dynamic rendering can also save an HTML representation of the block as a backup. If you provide a server-side rendering callback, the HTML representing the block in the database will be replaced with the output of your callback, but will be rendered if your block is deactivated (the plugin that registers the block is uninstalled) or your render callback is removed. + +In some cases, the block saves an HTML representation of the block and uses a dynamic rendering to fine-tune this markup if some conditions are met. Some examples of core blocks using this approach are: +- The [`cover`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/cover) block saves a [full HTML representation of the block in the database](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/cover/save.js). This markup is processed via a [`render_callback`](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/cover/index.php#L74) when requested to do some PHP magic that dynamically [injects the featured image if the "use featured image" setting is enabled](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/cover/index.php#L16). +- The [`image`](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/image) block also saves [its HTML representation in the database](https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/src/image/save.js) and processes it via a [`render_callback`](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/image/index.php#L363) when requested to [add some attributes to the markup](https://github.com/WordPress/gutenberg/blob/22741661998834e69db74ad863705ee2ce97b446/packages/block-library/src/image/index.php#L18) if some conditions are met. + +If you are using [InnerBlocks](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/nested-blocks-inner-blocks/) in a dynamic block, you will need to save the `InnerBlocks` in the `save` callback function using ``. + +## Additional Resources + +- [Static vs. dynamic blocks: What’s the difference?](https://developer.wordpress.org/news/2023/02/27/static-vs-dynamic-blocks-whats-the-difference/) +- [Block deprecation – a tutorial](https://developer.wordpress.org/news/2023/03/10/block-deprecation-a-tutorial/) \ No newline at end of file diff --git a/docs/getting-started/glossary.md b/docs/getting-started/glossary.md index bff8925e4619e1..ca509f6a321dba 100644 --- a/docs/getting-started/glossary.md +++ b/docs/getting-started/glossary.md @@ -70,7 +70,7 @@ This refers to a collection of features that ultimately allows users to edit the The CSS styles generated by WordPress and enqueued as an embedded stylesheet in the front end of the site. The stylesheet ID is `global-styles-inline-css`. The contents of this stylesheet come from the default `theme.json` of WordPress, the theme's `theme.json`, and the styles provided by the user via the global styles sidebar in the site editor. -See [theme.json reference docs](/docs/reference-guides/theme-json-reference.md), the [how to guide](/docs/how-to-guides/themes/theme-json.md), and an introduction to [styles in the block editor](/docs/explanations/architecture/styles.md). +See [theme.json reference docs](/docs/reference-guides/theme-json-reference.md), the [how to guide](/docs/how-to-guides/themes/global-settings-and-styles.md), and an introduction to [styles in the block editor](/docs/explanations/architecture/styles.md). Compare to block styles. diff --git a/docs/getting-started/quick-start-guide.md b/docs/getting-started/quick-start-guide.md index e978b250ab8aff..736a56c006c9e1 100644 --- a/docs/getting-started/quick-start-guide.md +++ b/docs/getting-started/quick-start-guide.md @@ -25,7 +25,7 @@ Navigate to the Plugins page of your local WordPress installation and activate t ## 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. +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 @@ -37,6 +37,14 @@ The `npm start` command will start a development server and watch for changes in When you are finished making changes, run the `npm run build` command. This optimizes the block code and makes it production-ready. +## View the block in action + +You can use any local WordPress development environment to test your new block, but the scaffolded plugin includes configuration for `wp-env`. You must have [Docker](https://www.docker.com/products/docker-desktop) already installed and running on your machine, but if you do, run the `npx wp-env start` command. + +Once the script finishes running, you can access the local environment at: http://localhost:8888. Log into the WordPress dashboard using username `admin` and password `password`. The plugin will already be installed and activated. Open the Editor or Site Editor, and insert the Copyright Date Block as you would any other block. + +Visit the [Getting started](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/) guide to learn more about `wp-env`. + ## Additional resources - [Get started with create-block](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) diff --git a/docs/getting-started/tutorial.md b/docs/getting-started/tutorial.md index e70b4aba9234eb..11dde6506472f7 100644 --- a/docs/getting-started/tutorial.md +++ b/docs/getting-started/tutorial.md @@ -36,9 +36,9 @@ The first step in creating the Copyright Date Block is to scaffold the initial b Review the Get started with create-block documentation for an introduction to using this package. -You can use `create-block` from just about any directory on your computer and then use `wp-env` to create a local WordPress development environment with your new block plugin installed and activated. +You can use `create-block` from just about any directory (folder) on your computer and then use `wp-env` to create a local WordPress development environment with your new block plugin installed and activated. -Therefore, create a new directory (folder) on your computer called "Block Tutorial". Open your terminal and `cd` to this directory. Then run the following command. +Therefore, choose a directory to place the block plugin or optionally create a new folder called "Block Tutorial". Open your terminal and `cd` to this directory. Then run the following command.
If you are not using wp-env, instead, navigate to the plugins/ folder in your local WordPress installation using the terminal and run the following command. @@ -203,7 +203,7 @@ Before you start building the functionality of the block itself, let's do a bit Open the [`index.js`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block/#index-js) file. This is the main JavaScript file of the block and is used to register it on the client. You can learn more about client-side and server-side registration in the [Registration of a block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/) documentation. -Start by looking at the [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/) function. This function accepts the name of the block, which we are getting from the imported `block.js` file, and the block configuration object. +Start by looking at the [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/) function. This function accepts the name of the block, which we are getting from the imported `block.json` file, and the block configuration object. ```js import Edit from './edit'; @@ -327,7 +327,7 @@ Save the file and confirm that the block appears correctly in the Editor and on ### Cleaning up -When you use the `create-block` package to scaffold a block, it might include files that you don't need. In the case of this tutorial, the block doesn't use stylesheets or font end JavaScipt. Clean up the plugin's `src/` folder with the following actions. +When you use the `create-block` package to scaffold a block, it might include files that you don't need. In the case of this tutorial, the block doesn't use stylesheets or front end JavaScript. Clean up the plugin's `src/` folder with the following actions. 1. In the `edit.js` file, remove the lines that import `editor.scss` 2. In the `index.js` file, remove the lines that import `style.scss` @@ -351,9 +351,10 @@ To enable this starting year functionality, you will need one attribute to store ### Updating block.json -Block attributes are generally specified in the [`block.json`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#data-storage-in-the-block-with-attributes) file. So open up the file and add the following section after the `example` in line 9. +Block attributes are generally specified in the [`block.json`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#data-storage-in-the-block-with-attributes) file. So open up the file and add the following section after the `example` property. ```json +"example": {}, "attributes": { "showStartingYear": { "type": "boolean" @@ -391,7 +392,7 @@ Next, update the Edit function to return the current block content and an `Inspe ```js export default function Edit() { -const currentYear = new Date().getFullYear().toString(); + const currentYear = new Date().getFullYear().toString(); return ( <> @@ -421,7 +422,7 @@ Then wrap the "Testing" message in the `PanelBody` component and set the `title` ```js export default function Edit() { -const currentYear = new Date().getFullYear().toString(); + const currentYear = new Date().getFullYear().toString(); return ( <> @@ -483,7 +484,7 @@ export default function Edit( { attributes, setAttributes } ) { 'Starting year', 'copyright-date-block' ) } - value={ startingYear } + value={ startingYear || '' } onChange={ ( value ) => setAttributes( { startingYear: value } ) } @@ -496,6 +497,10 @@ export default function Edit( { attributes, setAttributes } ) { } ``` +
+ You may have noticed that the value property has a value of startingYear || ''. The symbol || is called the Logical OR (logical disjunction) operator. This prevents warnings in React when the startingYear is empty. See Controlled and uncontrolled components for details. +
+ Save the file and refresh the Editor. Confirm that a text field now exists in the Settings panel. Add a starting year and confirm that when you update the page, the value is saved. ![A live look at editing the new Starting Year field in the Settings Sidebar](https://developer.wordpress.org/files/2023/12/block-tutorial-11.gif) @@ -522,7 +527,7 @@ export default function Edit( { attributes, setAttributes } ) { setAttributes( { startingYear: value } ) } @@ -601,7 +606,7 @@ export default function Edit( { attributes, setAttributes } ) { setAttributes( { startingYear: value } ) } @@ -690,7 +695,9 @@ In the next section, however, you will add static rendering to the block. This e ## Adding static rendering -A block can be dynamically rendered, statically rendered, or both. The block you have built so far is dynamically rendered. The HTML output of the block is not actually stored in the database, only the block markup and the associated attributes. +A block can utilize dynamic rendering, static rendering, or both. The block you have built so far is dynamically rendered. Its block markup and associated attributes are stored in the database, but its HTML output is not. + +Statically rendered blocks will always store the block markup, attributes, and output in the database. Blocks can also store static output in the database while being further enhanced dynamically on the front end, a combination of both methods. You will see the following if you switch to the Code editor from within the Editor. @@ -958,9 +965,9 @@ You will not get any block validation errors, but the Editor will detect that ch #### Optimizing render.php -The final step is to optimize the `render.php` file. If the `currentYear` and the `fallbackCurrentYear` attribute are the same, then there is no need to dynamically create the block content. It is already saved in the database and is available in the `render.php` file via the `$block_content` variable. +The final step is to optimize the `render.php` file. If the `currentYear` and the `fallbackCurrentYear` attribute are the same, then there is no need to dynamically create the block content. It is already saved in the database and is available in the `render.php` file via the `$content` variable. -Therefore, update the file to render the `$block_content` if `currentYear` and `fallbackCurrentYear` match. +Therefore, update the file to render the generated content if `currentYear` and `fallbackCurrentYear` do not match. ```php $current_year = date( "Y" ); diff --git a/docs/how-to-guides/README.md b/docs/how-to-guides/README.md index 152f8ce6184ae2..c57dcad7a35289 100644 --- a/docs/how-to-guides/README.md +++ b/docs/how-to-guides/README.md @@ -6,7 +6,7 @@ The new editor is highly flexible, like most of WordPress. You can build custom The editor is about blocks, and the main extensibility API is the Block API. It allows you to create your own static blocks, [Dynamic Blocks](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md) ( rendered on the server ) and also blocks capable of saving data to Post Meta for more structured content. -If you want to learn more about block creation, see the [Create a Block tutorial](/docs/getting-started/create-block/README.md) for the best place to start. +If you want to learn more about block creation, see the [Create a Block tutorial](/docs/getting-started/devenv/get-started-with-create-block.md) for the best place to start. ## Extending blocks diff --git a/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md b/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md index 697984c9456e02..41646bbe527cfb 100644 --- a/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md +++ b/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md @@ -6,7 +6,7 @@ A block typically inserts markup (HTML) into post content that you want to style ## Before you start -You will need a basic block and WordPress development environment to implement the examples shown in this guide. See the [create a basic block](/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md) or [block tutorial](/docs/getting-started/create-block/README.md) to get setup. +You will need a basic block and WordPress development environment to implement the examples shown in this guide. See the [create a basic block](/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md) or [block tutorial](/docs/getting-started/devenv/get-started-with-create-block.md) to get setup. ## Methods to add style diff --git a/docs/how-to-guides/curating-the-editor-experience.md b/docs/how-to-guides/curating-the-editor-experience.md deleted file mode 100644 index 1d14918df7eb5b..00000000000000 --- a/docs/how-to-guides/curating-the-editor-experience.md +++ /dev/null @@ -1,459 +0,0 @@ -# Curating the Editor Experience - -The purpose of this guide is to offer various ways one can lock down and curate the experience of using WordPress, especially with the introduction of more design tools and full site editing functionality. - -For information around adding functionality to a theme, rather than curating and locking, please review this guide on [Converting a classic theme to a block theme](https://developer.wordpress.org/themes/block-themes/converting-a-classic-theme-to-a-block-theme/). - -## Locking APIs - -**Lock the ability to move or remove specific blocks** - -Users have the ability to lock and unlock blocks via the editor. The locking UI has options for preventing blocks from being moved within the content canvas or removed: - -![Image of locking interface](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20interface.png?raw=true) - -Keep in mind that you can apply locking options to blocks nested inside of a containing block by turning on the "Apply to all blocks inside" option. However, you cannot mass lock blocks otherwise. - -**Lock the ability to edit certain blocks** - -Alongside the ability to lock moving or removing blocks, the [Navigation Block](https://github.com/WordPress/gutenberg/pull/44739) and [Reusable block](https://github.com/WordPress/gutenberg/pull/39950) have an additional capability: lock the ability to edit the contents of the block. This locks the ability to make changes to any blocks inside of either block type. - -**Apply block locking to patterns or templates** - -When building patterns or templates, theme authors can use these same UI tools to set the default locked state of blocks. For example, a theme author could lock various pieces of a header. Keep in mind that by default, users with editing access can unlock these blocks. [Here’s an example of a pattern](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805) with various blocks locked in different ways and here’s more context on [creating a template with locked blocks](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/). You can build these patterns in the editor itself, including adding locking options, before following the [documentation to register them](/docs/reference-guides/block-api/block-patterns.md). - -**Apply content only editing in patterns or templates** - -This functionality was introduced in WordPress 6.1. In contrast to block locking, which disables the ability to move or remove blocks, content only editing is both designed for use at the pattern or template level and hides all design tools, while still allowing for the ability to edit the content of the blocks. This provides a great way to simplify the interface for users and preserve a design. When this option is added, the following changes occur: - -- Non-content child blocks (containers, spacers, columns, etc) are hidden from list view, un-clickable on the canvas, and entirely un-editable. -- The Inspector will display a list of all child 'content' blocks. Clicking a block in this list reveals its settings panel. -- The main List View only shows content blocks, all at the same level regardless of actual nesting. -- Children blocks within the overall content locked container are automatically move / remove locked. -- Additional child blocks cannot be inserted, further preserving the design and layout. -- There is a link in the block toolbar to ‘Modify’ that a user can toggle on/off to have access to the broader design tools. Currently, it's not possibly to programmatically remove this option. - -This option can be applied to Columns, Cover, and Group blocks as well as third-party blocks that have the templateLock attribute in its block.json. To adopt this functionality, you need to use `"templateLock":"contentOnly"`. [Here's an example of a pattern](https://gist.github.com/annezazu/d62acd2514cea558be6cea97fe28ff3c) with this functionality in place. For more information, please [review the relevant documentation](/docs/reference-guides/block-api/block-templates.md#locking). - -Note: There is no UI in place to manage content locking and it must be managed at the code level. - -**Change permissions to control locking ability** - -Agencies and plugin authors can offer an even more curated experience by limiting which users have [permission to lock and unlock blocks](https://make.wordpress.org/core/2022/05/05/block-locking-settings-in-wordpress-6-0/). By default, anyone who is an administrator will have access to lock and unlock blocks. - -Developers can add a filter to the [block_editor_settings_all](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/) hook to configure permissions around locking blocks. The hook passes two parameters to the callback function: - -- `$settings` - An array of configurable settings for the editor. - -- `$context` - An instance of WP_Block_Editor_Context, an object that contains information about the current editor. - -Specifically, developers can alter the `$settings['canLockBlocks']` value by setting it to `true` or `false`, typically by running through one or more conditional checks. - -The following example disables block locking permissions for all users when editing a page: - -```php -add_filter( 'block_editor_settings_all', function( $settings, $context ) { - if ( $context->post && 'page' === $context->post->post_type ) { - $settings['canLockBlocks'] = false; - } - - return $settings; -}, 10, 2 ); -``` - -Another common use case may be to only allow users who can edit the visual design of the site (theme editing) to lock or unlock blocks. The best option would be to test against the `edit_theme_options` capability, as shown in the following code snippet: - -```php -add_filter( 'block_editor_settings_all', function( $settings ) { - $settings['canLockBlocks'] = current_user_can( 'edit_theme_options' ); - - return $settings; -} ); -``` - -Developers may use any type of conditional check to determine who can lock/unlock blocks. This is merely a small sampling of what is possible via the filter hook. - - -## Providing default controls/options - -**Define default options** - -Since theme.json acts as a configuration tool, there are numerous ways to define at a granular level what options are available. This section will use duotone as an example since it showcases a feature that cuts across a few blocks and allows for varying levels of access. - -*Duotone with Core options and customization available for each image related block:* - -```json -{ -"version": 2, - "settings": { - "color": { - "customDuotone": true, - "duotone": [ - ] - } - } -} -``` - -*Duotone with theme defined color options, Core options, and customization available for each image related block:* -```json -{ - "version": 2, - "settings": { - "color": { - "duotone": [ - { - "colors": [ "#000000", "#ffffff" ], - "slug": "foreground-and-background", - "name": "Foreground and background" - }, - { - "colors": [ "#000000", "#ff0200" ], - "slug": "foreground-and-secondary", - "name": "Foreground and secondary" - }, - { - "colors": [ "#000000", "#7f5dee" ], - "slug": "foreground-and-tertiary", - "name": "Foreground and tertiary" - }, - ] - } - } -} -``` - -*Duotone with defined default options and all customization available for the Post Featured Image block:* - -```json -{ - "schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, - "settings": { - "color": { - "custom": true, - "customDuotone": true - }, - "blocks": { - "core/post-featured-image": { - "color": { - "duotone": [ - { - "colors": [ "#282828", "#ff5837" ], - "slug": "black-and-orange", - "name": "Black and Orange" - }, - { - "colors": [ "#282828", "#0288d1" ], - "slug": "black-and-blue", - "name": "Black and Blue" - } - ], - "customDuotone": true, - "custom": true - } - } - } - } -} -``` - -*Duotone with only defined default options and core options available for the Post Featured Image block (no customization):* - -```json -{ - "schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, - "settings": { - "color": { - "custom": true, - "customDuotone": true - }, - "blocks": { - "core/post-featured-image": { - "color": { - "duotone": [ - { - "colors": [ "#282828", "#ff5837" ], - "slug": "black-and-orange", - "name": "Black and Orange" - }, - { - "colors": [ "#282828", "#0288d1" ], - "slug": "black-and-blue", - "name": "Black and Blue" - } - ], - "customDuotone": false, - "custom": false - } - } - } - } -} -``` - -## Limiting interface options with theme.json - -**Limit options on a per block basis** - -Beyond defining default values, using theme.json allows you to also remove options entirely and instead rely on what the theme has set in place. Below is a visual showing two extremes with the same paragraph block: - -![Image of restricted interface](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20comparison%20visual.png?raw=true) - -Continuing the examples with duotone, this means you could allow full access to all Duotone functionality for Image blocks and only limit the Post Featured Image block like so: - -```json -{ - "schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, - "settings": { - "color": { - "custom": true, - "customDuotone": true - }, - "blocks": { - "core/image": { - "color": { - "duotone": [], - "customDuotone": true, - "custom": true - } - }, - "core/post-featured-image": { - "color": { - "duotone": [], - "customDuotone": false, - "custom": false - } - } - } - } -} -``` - -You can read more about how best to [turn on/off options with theme.json here](/docs/how-to-guides/themes/theme-json.md). - -**Disable inherit default layout** - -To disable the “Inherit default layout” setting for container blocks like the Group block, remove the following section: - -```json -"layout": { - "contentSize": null, - "wideSize": null -}, -``` - -**Limit options globally** - -When using theme.json in a block or classic theme, these settings will stop the default color and typography controls from being enabled globally, greatly limiting what’s possible: - -```json -{ - "$schema": "http://schemas.wp.org/trunk/theme.json", - "version": 2, - "settings": { - "layout": { - "contentSize": "750px" - }, - "color": { - "background": false, - "custom": false, - "customDuotone": false, - "customGradient": false, - "defaultGradients": false, - "defaultPalette": false, - "text": false - }, - "typography": { - "customFontSize": false, - "dropCap": false, - "fontStyle": false, - "fontWeight": false, - "letterSpacing": false, - "lineHeight": false, - "textDecoration": false, - "textTransform": false - } - } -} -``` - -To enable something from the above, just set whatever value you want to change to `true` for more granularity. - -## Limiting interface options with theme.json filters - -The theme.json file is a great way to control interface options, but it only allows for global or block-level modifications, which can be limiting in some scenarios. - -For instance, in the previous section, color and typography controls were disabled globally using theme.json. But let's say you want to enable color settings for users who are Administrators. - -To provide more flexibility, WordPress 6.1 introduced server-side filters allowing you to customize theme.json data at four different data layers. - -- [`wp_theme_json_data_default`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_default/) - Hooks into the default data provided by WordPress -- [`wp_theme_json_data_blocks`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_blocks/) - Hooks into the data provided by blocks. -- [`wp_theme_json_data_theme`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_theme/) - Hooks into the data provided by the current theme. -- [`wp_theme_json_data_user`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_user/) - Hooks into the data provided by the user. - -In the following example, the data from the current theme's theme.json file is updated using the `wp_theme_json_data_theme` filter. Color controls are restored if the current user is an Administrator. - -```php -// Disable color controls for all users except Administrators. -function example_filter_theme_json_data_theme( $theme_json ){ - $is_administrator = current_user_can( 'edit_theme_options' ); - - if ( $is_administrator ) { - $new_data = array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'background' => true, - 'custom' => true, - 'customDuotone' => true, - 'customGradient' => true, - 'defaultGradients' => true, - 'defaultPalette' => true, - 'text' => true, - ), - ), - ); - } - - return $theme_json->update_with( $new_data ); -} -add_filter( 'wp_theme_json_data_theme', 'example_filter_theme_json_data_theme' ); -``` - -The filter receives an instance of the `WP_Theme_JSON_Data class` with the data for the respective layer. Then, you pass new data in a valid theme.json-like structure to the `update_with( $new_data )` method. A theme.json version number is required in `$new_data`. - - -## Limiting interface options with client-side filters - -WordPress 6.2 introduced a new client-side filter allowing you to modify block-level [theme.json settings](/docs/reference-guides/theme-json-reference/theme-json-living.md#settings) before the editor is rendered. - -The filter is called `blockEditor.useSetting.before` and can be used in the JavaScript code as follows: - -```js -import { addFilter } from '@wordpress/hooks'; - -/** - * Limit the Column block's spacing options to pixels. - */ -addFilter( - 'blockEditor.useSetting.before', - 'example/useSetting.before', - ( settingValue, settingName, clientId, blockName ) => { - if ( blockName === 'core/column' && settingName === 'spacing.units' ) { - return [ 'px' ]; - } - return settingValue; - } -); -``` - -This example will restrict the available spacing units for the Column block to just pixels. As discussed above, a similar restriction could be applied using theme.json filters or directly in a theme’s theme.json file using block-level settings. - -However, the `blockEditor.useSetting.before` filter is unique because it allows you to modify settings according to the block’s location, neighboring blocks, the current user’s role, and more. The possibilities for customization are extensive. - -In the following example, text color controls are disabled for the Heading block whenever the block is placed inside of a Media & Text block. - -```js -import { select } from '@wordpress/data'; -import { addFilter } from '@wordpress/hooks'; - -/** - * Disable text color controls on Heading blocks when placed inside of Media & Text blocks. - */ -addFilter( - 'blockEditor.useSetting.before', - 'example/useSetting.before', - ( settingValue, settingName, clientId, blockName ) => { - if ( blockName === 'core/heading' ) { - const { getBlockParents, getBlockName } = select( 'core/block-editor' ); - const blockParents = getBlockParents( clientId, true ); - const inMediaText = blockParents.some( ( ancestorId ) => getBlockName( ancestorId ) === 'core/media-text' ); - - if ( inMediaText && settingName === 'color.text' ) { - return false; - } - } - - return settingValue; - } -); -``` - -## Remove access to functionality - -**Remove access to the template editor** - -Whether you’re using [theme.json in a Classic Theme](https://developer.wordpress.org/themes/block-themes/converting-a-classic-theme-to-a-block-theme/#adding-theme-json-in-classic-themes) or Block Theme, you can add the following to your functions.php file to remove access to the Template Editor that is available when editing posts or pages: - -`remove_theme_support( 'block-templates');` - -This prevents both the ability to both create new block templates or edit them from within the Post Editor. - -**Create an allow or disallow list to limit block options** - -There might be times when you don’t want access to a block at all to be available for users. To control what’s available in the inserter, you can take two approaches: [an allow list](/docs/reference-guides/filters/block-filters.md#using-an-allow-list) that disables all blocks except those on the list or a [deny list that unregisters specific blocks](/docs/reference-guides/filters/block-filters.md#using-a-deny-list). - -**Disable pattern directory** - -To fully remove patterns bundled with WordPress core from being accessed in the Inserter, the following can be added to your functions.php file: - -`remove_theme_support( 'core-block-patterns' );` - -## Utilizing patterns - -**Prioritize starter patterns for any post type** - -When a user creates new content, regardless of post type, they are met with an empty canvas. However, that experience can be improved thanks to the option to have patterns from a specific type prioritized upon creation of a new piece of content. The modal appears each time the user creates a new item when there are patterns on their website that declare support for the `core/post-content` block types. By default, WordPress does not include any of these patterns, so the modal will not appear without at least two of these post content patterns being added. - -To opt into this, include `core/post-content` in the Block Types for your pattern. From there you can control which post types the pattern should show up for via the Post Types option. [Here's an example of a pattern](https://gist.github.com/annezazu/ead4c4965345251ec999b716c0c84f32) that would appear when creating a new post. - -Read more about this functionality in the [Page creation patterns in WordPress 6.0 dev note](https://make.wordpress.org/core/2022/05/03/page-creation-patterns-in-wordpress-6-0/) and [note that WordPress 6.1 brought this functionality to all post types](https://make.wordpress.org/core/2022/10/10/miscellaneous-editor-changes-for-wordpress-6-1/#start-content-patterns-for-all-post-types). - -**Prioritize starter patterns for template creation** - -In the same way patterns can be prioritized for new posts or pages, the same experience can be added to the template creation process. When patterns declare support for the 'templateTypes' property, the patterns will appear anytime a template that matches the designation is created, along with the options to start from a blank state or use the current fallback of the template. By default, WordPress does not include any of these patterns. - -To opt into this, a pattern needs to specify a property called `templateTypes`, which is an array containing the templates where the patterns can be used as the full content. Here's an example of a pattern that would appear when creating a 404 template: - -``` -register_block_pattern( - 'wp-my-theme/404-template-pattern', - array( - 'title' => __( '404 Only template pattern', 'wp-my-theme' ), - 'templateTypes' => array( '404' ), - 'content' => '

404 pattern

', - ) -); -``` - -Read more about this functionality in the [Patterns on the create a new template modal in the WordPress 6.3 dev note](https://make.wordpress.org/core/2023/07/18/miscellaneous-editor-changes-in-wordpress-6-3/#patterns-on-the-create-a-new-template-modal). - -**Lock patterns** - -As mentioned in the prior section on Locking APIs, aspects of patterns themselves can be locked so that the important aspects of the design can be preserved. [Here’s an example of a pattern](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805) with various blocks locked in different ways. You can build these patterns in the editor itself, including adding locking options, before [following the documentation to register them](/docs/reference-guides/block-api/block-patterns.md). - -**Prioritize specific patterns from the Pattern Directory** - -With WordPress 6.0 themes can register patterns from [Pattern Directory](https://wordpress.org/patterns/) through theme.json. To accomplish this, themes should use the new patterns top level key in theme.json. Within this field, themes can list patterns to register from the Pattern Directory. The patterns field is an array of pattern slugs from the Pattern Directory. Pattern slugs can be extracted by the url in a single pattern view at the Pattern Directory. Example: This url https://wordpress.org/patterns/pattern/partner-logos the slug is partner-logos. -```json -{ - "version": 2, - "patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ] -} -``` - -Note that this field requires using [version 2 of theme.json](/docs/reference-guides/theme-json-reference/theme-json-living.md). The content creator will then find the respective Pattern in the inserter “Patterns” tab in the categories that match the categories from the Pattern Directory. - -## Combining approaches - -Keep in mind that the above approaches can be combined as you see fit. For example, you can provide custom patterns to use when creating a new page while also limiting the amount of customization that can be done to aspects of them, like only allowing certain preset colors to be used for the background of a Cover block or locking down what blocks can be deleted. When considering the approaches to take, think about the specific ways you might want to both open up the experience and curate it. - -## Additional Resources - -- [Builder Basics – Working with Templates in Full Site Editing (Part 3)](https://wordpress.tv/2022/05/24/nick-diego-builder-basics-working-with-templates-in-full-site-editing-part-3/) -- [Core Editor Improvement: Curated experiences with locking APIs & theme.json](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/) -- [Learn WordPress session on Curating the Editor Experience](https://wordpress.tv/2022/07/22/nick-diego-curating-the-editor-experience/) diff --git a/docs/how-to-guides/curating-the-editor-experience/README.md b/docs/how-to-guides/curating-the-editor-experience/README.md new file mode 100644 index 00000000000000..3b348d67dee26d --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/README.md @@ -0,0 +1,25 @@ +# Curating the Editor Experience + +Curating the editing experience in WordPress is important because it allows you to streamline the editing process, ensuring consistency and alignment with the site's style and branding guidelines. It also makes it easier for users to create and manage content effectively without accidental modifications or layout changes. This leads to a more efficient and personalized experience. + +The purpose of this guide is to offer various ways you can lock down and curate the experience of using WordPress, especially with the introduction of more design tools and the Site Editor. + +In this section, you will learn: + +1. [**Block locking**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/block-locking): how to restrict user interactions with specific blocks in the Editor for better content control +1. [**Patterns**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/patterns): about creating and implementing predefined block layouts to ensure design and content uniformity +1. [**theme.json**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/theme-json): to configure global styles and settings for your theme using the theme.json file +1. [**Filters and hooks**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/filters-and-hooks): about the essential filters and hooks used to modify the Editor +1. [**Disabling Editor functionality**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/disable-editor-functionality): about additional ways to selectively disable features or components in the Editor to streamline the user experience + +## Combining approaches + +Remember that the approaches provided in the documentation above can be combined as you see fit. For example, you can provide custom patterns to use when creating a new page while also limiting the amount of customization that can be done to aspects of them, like only allowing specific preset colors to be used for the background of a Cover block or locking down what blocks can be deleted. + +When considering the approaches to take, think about the specific ways you might want to both open up the experience and curate it. + +## Additional resources + +- [Builder Basics – Working with Templates in Full Site Editing (Part 3)](https://wordpress.tv/2022/05/24/nick-diego-builder-basics-working-with-templates-in-full-site-editing-part-3/) +- [Core Editor Improvement: Curated experiences with locking APIs & theme.json](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/) +- [Learn WordPress session on Curating the Editor Experience](https://wordpress.tv/2022/07/22/nick-diego-curating-the-editor-experience/) diff --git a/docs/how-to-guides/curating-the-editor-experience/block-locking.md b/docs/how-to-guides/curating-the-editor-experience/block-locking.md new file mode 100644 index 00000000000000..83f26ea87a479e --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/block-locking.md @@ -0,0 +1,69 @@ +# Block Locking API + +The Block Locking API allows you to restrict actions on specific blocks within the Editor. This API can be used to prevent users from moving, removing, or editing certain blocks, ensuring layout consistency and content integrity. + +## Lock the ability to move or remove specific blocks + +Users can lock and unlock blocks via the Editor. The locking UI has options for preventing blocks from being moved within the content canvas or removed: + +![Image of locking interface](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20interface.png?raw=true) + +Keep in mind that you can apply locking options to blocks nested inside of a containing block by turning on the "Apply to all blocks inside" option. However, you cannot mass lock blocks otherwise. + +## Lock the ability to edit certain blocks + +Alongside the ability to lock moving or removing blocks, the [Navigation Block](https://github.com/WordPress/gutenberg/pull/44739) and [Reusable block](https://github.com/WordPress/gutenberg/pull/39950) have an additional capability: lock the ability to edit the contents of the block. This locks the ability to make changes to any blocks inside of either block type. + +## Apply block locking to patterns or templates + +When building patterns or templates, theme authors can use these same UI tools to set the default locked state of blocks. For example, a theme author could lock various pieces of a header. Keep in mind that by default, users with editing access can unlock these blocks. [Here’s an example of a pattern](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805) with various blocks locked in different ways and here’s more context on [creating a template with locked blocks](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/). You can build these patterns in the Editor itself, including adding locking options, before following the [documentation to register them](/docs/reference-guides/block-api/block-patterns.md). + +## Apply content-only editing in patterns or templates + +This functionality was introduced in WordPress 6.1. In contrast to block locking, which disables the ability to move or remove blocks, content-only editing is both designed for use at the pattern or template level and hides all design tools, while still allowing for the ability to edit the content of the blocks. This provides a great way to simplify the interface for users and preserve a design. When this option is added, the following changes occur: + +- Non-content child blocks (containers, spacers, columns, etc) are hidden from list view, un-clickable on the canvas, and entirely un-editable. +- The Inspector will display a list of all child 'content' blocks. Clicking a block in this list reveals its settings panel. +- The main List View only shows content blocks, all at the same level regardless of actual nesting. +- Children blocks within the overall content locked container are automatically move / remove locked. +- Additional child blocks cannot be inserted, further preserving the design and layout. +- There is a link in the block toolbar to ‘Modify’ that a user can toggle on/off to have access to the broader design tools. Currently, it's not possibly to programmatically remove this option. + +This option can be applied to Columns, Cover, and Group blocks as well as third-party blocks that have the templateLock attribute in its block.json. To adopt this functionality, you need to use `"templateLock":"contentOnly"`. [Here's an example of a pattern](https://gist.github.com/annezazu/d62acd2514cea558be6cea97fe28ff3c) with this functionality in place. For more information, please [review the relevant documentation](/docs/reference-guides/block-api/block-templates.md#locking). + +Note: There is no UI in place to manage content locking and it must be managed at the code level. + +## Change permissions to control locking ability + +Agencies and plugin authors can offer an even more curated experience by limiting which users have [permission to lock and unlock blocks](https://make.wordpress.org/core/2022/05/05/block-locking-settings-in-wordpress-6-0/). By default, anyone who is an administrator will have access to lock and unlock blocks. + +Developers can add a filter to the [block_editor_settings_all](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/) hook to configure permissions around locking blocks. The hook passes two parameters to the callback function: + +- `$settings` - An array of configurable settings for the Editor. +- `$context` - An instance of WP_Block_Editor_Context, an object that contains information about the current Editor. + +Specifically, developers can alter the `$settings['canLockBlocks']` value by setting it to `true` or `false`, typically by running through one or more conditional checks. + +The following example disables block locking permissions for all users when editing a page: + +```php +add_filter( 'block_editor_settings_all', function( $settings, $context ) { + if ( $context->post && 'page' === $context->post->post_type ) { + $settings['canLockBlocks'] = false; + } + + return $settings; +}, 10, 2 ); +``` + +Another common use case may be to only allow users who can edit the visual design of the site (theme editing) to lock or unlock blocks. Now, the best option would be to test against the `edit_theme_options` capability, as shown in the following code snippet: + +```php +add_filter( 'block_editor_settings_all', function( $settings ) { + $settings['canLockBlocks'] = current_user_can( 'edit_theme_options' ); + + return $settings; +} ); +``` + +Developers may use any type of conditional check to determine who can lock/unlock blocks. This is merely a small sampling of what is possible via the filter hook. \ No newline at end of file diff --git a/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md b/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md new file mode 100644 index 00000000000000..23803888f95221 --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md @@ -0,0 +1,89 @@ +# Disable Editor functionality + +This page is dedicated to the many ways you can disable specific functionality in the Post Editor and Site Editor that are not covered in other areas of the curation documentation. + +## Restrict block options + +There might be times when you don’t want access to a block at all to be available for users. To control what’s available in the inserter, you can take two approaches: [an allow list](/docs/reference-guides/filters/block-filters.md#using-an-allow-list) that disables all blocks except those on the list or a [deny list that unregisters specific blocks](/docs/reference-guides/filters/block-filters.md#using-a-deny-list). + +## Disable the Pattern Directory + +To fully remove patterns bundled with WordPress core from being accessed in the Inserter, the following can be added to your `functions.php` file: + +```php +function example_theme_support() { + remove_theme_support( 'core-block-patterns' ); +} +add_action( 'after_setup_theme', 'example_theme_support' ); +``` + +## Disable block variations + +Some Core blocks are actually [block variations](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/). A great example is the Row and Stack blocks, which are actually variations of the Group block. If you want to disable these "blocks", you actually need to disable the respective variations. + +Block variations are registered using JavaScript and need to be disabled with JavaScript. The code below will disable the Row variation. + +```js +wp.domReady( () => { + wp.blocks.unregisterBlockVariation( 'core/group', 'group-row' ); +}); +``` + +Assuming the code was placed in a `disable-variations.js` file located in the root of your theme folder, you can enqueue this file in the theme's `functions.php` using the code below. + +```php +function example_disable_variations_script() { + wp_enqueue_script( + 'example-disable-variations-script', + get_template_directory_uri() . '/disable-variations.js', + array( 'wp-dom-ready' ), + wp_get_theme()->get( 'Version' ), + true + ); +} +add_action( 'enqueue_block_editor_assets', 'example_disable_variations_script' ); +``` + +## Disable block styles + +There are a few Core blocks that include their own [block styles](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/). An example is the Image block, which includes a block style for rounded images called "Rounded". You many not want your users to round images, or you might prefer to use the border-radius control instead of the block style. Either way, it's easy to disable any unwanted block styles. + +Unlike block variations, you can register styles in either JavaScript or PHP. If a style was registered in JavaScript, it must be disabled with JavaScript. If registered using PHP, the style can be disabled with either. All Core block styles are registed in JavaScript. + +So, you would use the following code to disable the "Rounded" block style for the Image block. + +```js +wp.domReady( () => { + wp.blocks.unregisterBlockStyle( 'core/image', 'rounded' ); +}); +``` + +This JavaScript should be enqueued much like the block variation example above. Refer to the [block styles](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/) documentation for how to register and unregister styles using PHP. + +## Disable access to the Template Editor + +Whether you’re using theme.json in a Classic or Block theme, you can add the following to your `functions.php` file to remove access to the Template Editor that is available when editing posts or pages: + +```php +function example_theme_support() { + remove_theme_support( 'block-templates'); +} +add_action( 'after_setup_theme', 'example_theme_support' ); +``` + +This prevents both the ability to create new block templates or edit them from within the Post Editor. + +## Disable access to the Code Editor + +The Code Editor allows you to view the underlying block markup for a page or post. While this view is handy for experienced users, you can inadvertently break block markup by editing content. Add the following to your `functions.php` file to restrict access. + +```php +function example_restrict_code_editor_access( $settings, $context ) { + $settings[ 'codeEditingEnabled' ] = false; + + return $settings; +} +add_filter( 'block_editor_settings_all', 'example_restrict_code_editor_access', 10, 2 ); +``` + +This code prevents all users from accessing the Code Editor. You could also add [capability](https://wordpress.org/documentation/article/roles-and-capabilities/) checks to disable access for specific users. \ No newline at end of file diff --git a/docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md b/docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md new file mode 100644 index 00000000000000..fbc4de12586396 --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md @@ -0,0 +1,109 @@ +# Filters and hooks + +The Editor provides numerous filters and hooks that allow you to modify the editing experience. Here are a few. + +## Server-side theme.json filters + +The theme.json file is a great way to control interface options, but it only allows for global or block-level modifications, which can be limiting in some scenarios. + +For instance, in the previous section, color and typography controls were disabled globally using theme.json. But let's say you want to enable color settings for users who are Administrators. + +To provide more flexibility, WordPress 6.1 introduced server-side filters allowing you to customize theme.json data at four different data layers. + +- [`wp_theme_json_data_default`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_default/) - Hooks into the default data provided by WordPress +- [`wp_theme_json_data_blocks`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_blocks/) - Hooks into the data provided by blocks. +- [`wp_theme_json_data_theme`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_theme/) - Hooks into the data provided by the current theme. +- [`wp_theme_json_data_user`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_user/) - Hooks into the data provided by the user. + +In the following example, the data from the current theme's theme.json file is updated using the `wp_theme_json_data_theme` filter. Color controls are restored if the current user is an Administrator. + +```php +// Disable color controls for all users except Administrators. +function example_filter_theme_json_data_theme( $theme_json ){ + $is_administrator = current_user_can( 'edit_theme_options' ); + + if ( $is_administrator ) { + $new_data = array( + 'version' => 2, + 'settings' => array( + 'color' => array( + 'background' => true, + 'custom' => true, + 'customDuotone' => true, + 'customGradient' => true, + 'defaultGradients' => true, + 'defaultPalette' => true, + 'text' => true, + ), + ), + ); + } + + return $theme_json->update_with( $new_data ); +} +add_filter( 'wp_theme_json_data_theme', 'example_filter_theme_json_data_theme' ); +``` + +The filter receives an instance of the `WP_Theme_JSON_Data class` with the data for the respective layer. Then, you pass new data in a valid theme.json-like structure to the `update_with( $new_data )` method. A theme.json version number is required in `$new_data`. + + +## Client-side (Editor) filters + +WordPress 6.2 introduced a new client-side filter allowing you to modify block-level [theme.json settings](/docs/reference-guides/theme-json-reference/theme-json-living.md#settings) before the Editor is rendered. + +The filter is called `blockEditor.useSetting.before` and can be used in the JavaScript code as follows: + +```js +import { addFilter } from '@wordpress/hooks'; + +/** + * Limit the Column block's spacing options to pixels. + */ +addFilter( + 'blockEditor.useSetting.before', + 'example/useSetting.before', + ( settingValue, settingName, clientId, blockName ) => { + if ( blockName === 'core/column' && settingName === 'spacing.units' ) { + return [ 'px' ]; + } + return settingValue; + } +); +``` + +This example will restrict the available spacing units for the Column block to just pixels. As discussed above, a similar restriction could be applied using theme.json filters or directly in a theme’s theme.json file using block-level settings. + +However, the `blockEditor.useSetting.before` filter is unique because it allows you to modify settings according to the block’s location, neighboring blocks, the current user’s role, and more. The possibilities for customization are extensive. + +In the following example, text color controls are disabled for the Heading block whenever the block is placed inside of a Media & Text block. + +```js +import { select } from '@wordpress/data'; +import { addFilter } from '@wordpress/hooks'; + +/** + * Disable text color controls on Heading blocks when placed inside of Media & Text blocks. + */ +addFilter( + 'blockEditor.useSetting.before', + 'example/useSetting.before', + ( settingValue, settingName, clientId, blockName ) => { + if ( blockName === 'core/heading' ) { + const { getBlockParents, getBlockName } = select( 'core/block-editor' ); + const blockParents = getBlockParents( clientId, true ); + const inMediaText = blockParents.some( ( ancestorId ) => getBlockName( ancestorId ) === 'core/media-text' ); + + if ( inMediaText && settingName === 'color.text' ) { + return false; + } + } + + return settingValue; + } +); +``` + +## Additional resources + +- [How to modify theme.json data using server-side filters](https://developer.wordpress.org/news/2023/07/05/how-to-modify-theme-json-data-using-server-side-filters/) (WordPress Developer Blog) +- [Curating the Editor experience with client-side filters](https://developer.wordpress.org/news/2023/05/24/curating-the-editor-experience-with-client-side-filters/) (WordPress Developer Blog) \ No newline at end of file diff --git a/docs/how-to-guides/curating-the-editor-experience/patterns.md b/docs/how-to-guides/curating-the-editor-experience/patterns.md new file mode 100644 index 00000000000000..fbe5143298cdba --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/patterns.md @@ -0,0 +1,91 @@ +# Patterns + +Block [patterns](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-patterns/) are one of the best ways to provide users with unique and curated editing experiences. + +## Prioritize starter patterns for any post type + +When a user creates new content, regardless of post type, they are met with an empty canvas. However, that experience can be improved thanks to the option to have patterns from a specific type prioritized upon creation of a new piece of content. The modal appears each time the user creates a new item when there are patterns on their website that declare support for the `core/post-content` block types. By default, WordPress does not include any of these patterns, so the modal will not appear without at least two of these post content patterns being added. + +To opt into this, include `core/post-content` in the Block Types for your pattern. From there, you can control which post types the pattern should show up for via the Post Types option. Here's an example of a pattern that would appear when creating a new post. + +```php + + + +

Details

+ + + +

Directions

+ + + +

RSVP

+ + + +

To RSVP, please join the #fse-outreach-experiment in Make Slack.

+ + + + + + + +
+

We hope to see you there!

+
+ +``` + +Read more about this functionality in the [Page creation patterns in WordPress 6.0 dev note](https://make.wordpress.org/core/2022/05/03/page-creation-patterns-in-wordpress-6-0/) and [note that WordPress 6.1 brought this functionality to all post types](https://make.wordpress.org/core/2022/10/10/miscellaneous-editor-changes-for-wordpress-6-1/#start-content-patterns-for-all-post-types). + +## Prioritize starter patterns for template creation + +In the same way patterns can be prioritized for new posts or pages, the same experience can be added to the template creation process. When patterns declare support for the 'templateTypes' property, the patterns will appear anytime a template that matches the designation is created, along with the options to start from a blank state or use the current fallback of the template. By default, WordPress does not include any of these patterns. + +To opt into this, a pattern needs to specify a property called `templateTypes`, which is an array containing the templates where the patterns can be used as the full content. Here's an example of a pattern that would appear when creating a 404 template: + +```php +register_block_pattern( + 'wp-my-theme/404-template-pattern', + array( + 'title' => __( '404 Only template pattern', 'wp-my-theme' ), + 'templateTypes' => array( '404' ), + 'content' => '

404 pattern

', + ) +); +``` + +Read more about this functionality in the [Patterns on the create a new template modal in the WordPress 6.3 dev note](https://make.wordpress.org/core/2023/07/18/miscellaneous-editor-changes-in-wordpress-6-3/#patterns-on-the-create-a-new-template-modal). + +## Lock patterns + +As mentioned in the prior section on Locking APIs, aspects of patterns themselves can be locked so that the important aspects of the design can be preserved. [Here’s an example of a pattern](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805) with various blocks locked in different ways. You can build these patterns in the editor itself, including adding locking options, before [following the documentation to register them](/docs/reference-guides/block-api/block-patterns.md). + +## Prioritize specific patterns from the Pattern Directory + +With WordPress 6.0 themes can register patterns from [Pattern Directory](https://wordpress.org/patterns/) through theme.json. To accomplish this, themes should use the new patterns top level key in theme.json. Within this field, themes can list patterns to register from the Pattern Directory. The patterns field is an array of pattern slugs from the Pattern Directory. Pattern slugs can be extracted by the url in a single pattern view at the Pattern Directory. Example: This url https://wordpress.org/patterns/pattern/partner-logos the slug is partner-logos. + +```json +{ + "version": 2, + "patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ] +} +``` + +Note that this field requires using [version 2 of theme.json](/docs/reference-guides/theme-json-reference/theme-json-living.md). The content creator will then find the respective Pattern in the inserter “Patterns” tab in the categories that match the categories from the Pattern Directory. + +## Additional resources + +- [Using template patterns to build multiple homepage designs](https://developer.wordpress.org/news/2023/04/13/using-template-patterns-to-build-multiple-homepage-designs/) (WordPress Developer Blog) \ No newline at end of file diff --git a/docs/how-to-guides/curating-the-editor-experience/theme-json.md b/docs/how-to-guides/curating-the-editor-experience/theme-json.md new file mode 100644 index 00000000000000..d373e0e81e345a --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/theme-json.md @@ -0,0 +1,210 @@ +# theme.json + +A theme's theme.json file is one of the best ways to curate the Editor experience and will likely be the first tool you use before reaching for more sophisticated solutions. + +## Providing default controls/options + +Since theme.json acts as a configuration tool, there are numerous ways to define at a granular level what options are available. This section will use duotone as an example since it showcases a feature that cuts across a few blocks and allows for varying levels of access. + +*Duotone with Core options and customization available for each image related block:* + +```json +{ +"version": 2, + "settings": { + "color": { + "customDuotone": true, + "duotone": [ + ] + } + } +} +``` + +*Duotone with theme defined color options, Core options, and customization available for each image related block:* + +```json +{ + "version": 2, + "settings": { + "color": { + "duotone": [ + { + "colors": [ "#000000", "#ffffff" ], + "slug": "foreground-and-background", + "name": "Foreground and background" + }, + { + "colors": [ "#000000", "#ff0200" ], + "slug": "foreground-and-secondary", + "name": "Foreground and secondary" + }, + { + "colors": [ "#000000", "#7f5dee" ], + "slug": "foreground-and-tertiary", + "name": "Foreground and tertiary" + }, + ] + } + } +} +``` + +*Duotone with defined default options and all customization available for the Post Featured Image block:* + +```json +{ + "schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "color": { + "custom": true, + "customDuotone": true + }, + "blocks": { + "core/post-featured-image": { + "color": { + "duotone": [ + { + "colors": [ "#282828", "#ff5837" ], + "slug": "black-and-orange", + "name": "Black and Orange" + }, + { + "colors": [ "#282828", "#0288d1" ], + "slug": "black-and-blue", + "name": "Black and Blue" + } + ], + "customDuotone": true, + "custom": true + } + } + } + } +} +``` + +*Duotone with only defined default options and core options available for the Post Featured Image block (no customization):* + +```json +{ + "schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "color": { + "custom": true, + "customDuotone": true + }, + "blocks": { + "core/post-featured-image": { + "color": { + "duotone": [ + { + "colors": [ "#282828", "#ff5837" ], + "slug": "black-and-orange", + "name": "Black and Orange" + }, + { + "colors": [ "#282828", "#0288d1" ], + "slug": "black-and-blue", + "name": "Black and Blue" + } + ], + "customDuotone": false, + "custom": false + } + } + } + } +} +``` + +## Limiting interface options with theme.json + +### Limit options on a per-block basis + +Beyond defining default values, using theme.json allows you to also remove options entirely and instead rely on what the theme has set in place. Below is a visual showing two extremes with the same paragraph block: + +![Image of restricted interface](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20comparison%20visual.png?raw=true) + +Continuing the examples with duotone, this means you could allow full access to all Duotone functionality for Image blocks and only limit the Post Featured Image block like so: + +```json +{ + "schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "color": { + "custom": true, + "customDuotone": true + }, + "blocks": { + "core/image": { + "color": { + "duotone": [], + "customDuotone": true, + "custom": true + } + }, + "core/post-featured-image": { + "color": { + "duotone": [], + "customDuotone": false, + "custom": false + } + } + } + } +} +``` + +You can read more about how best to [turn on/off options with theme.json here](/docs/how-to-guides/themes/global-settings-and-styles.md). + +### Disable inherit default layout + +To disable the “Inherit default layout” setting for container blocks like the Group block, remove the following section: + +```json +"layout": { + "contentSize": null, + "wideSize": null +}, +``` + +### Limit options globally + +When using theme.json in a block or classic theme, these settings will stop the default color and typography controls from being enabled globally, greatly limiting what’s possible: + +```json +{ + "$schema": "http://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "layout": { + "contentSize": "750px" + }, + "color": { + "background": false, + "custom": false, + "customDuotone": false, + "customGradient": false, + "defaultGradients": false, + "defaultPalette": false, + "text": false + }, + "typography": { + "customFontSize": false, + "dropCap": false, + "fontStyle": false, + "fontWeight": false, + "letterSpacing": false, + "lineHeight": false, + "textDecoration": false, + "textTransform": false + } + } +} +``` + +To enable something from the above, just set whatever value you want to change to `true` for more granularity. \ No newline at end of file diff --git a/docs/how-to-guides/metabox.md b/docs/how-to-guides/metabox.md index b1baac1f255855..9db89f51c748d8 100644 --- a/docs/how-to-guides/metabox.md +++ b/docs/how-to-guides/metabox.md @@ -18,7 +18,7 @@ This guide shows how to create a block that prompts a user for a single value, a This guide assumes you are already familiar with WordPress plugins, post meta, and basic JavaScript. Review the [Getting started with JavaScript tutorial](/docs/how-to-guides/javascript/README.md) for an introduction. -The guide will walk through creating a basic block, but recommended to go through the [Create Block tutorial](/docs/getting-started/create-block/README.md) for a deeper understanding of creating custom blocks. +The guide will walk through creating a basic block, but recommended to go through the [Create Block tutorial](/docs/getting-started/devenv/get-started-with-create-block.md) for a deeper understanding of creating custom blocks. You will need: diff --git a/docs/how-to-guides/themes/README.md b/docs/how-to-guides/themes/README.md index 708ecba03d264d..1510b20c30047f 100644 --- a/docs/how-to-guides/themes/README.md +++ b/docs/how-to-guides/themes/README.md @@ -6,7 +6,7 @@ The block editor provides a number of options for theme designers and developers ### Classic theme -In terms of block editor terminology this is any theme that defines its templates in the traditional `.php` file format, and that doesn't have an `index.html` format template in the `/block-templates` or `/templates` folders. A `Classic` theme has the ability to provide configuration and styling options to the block editor, and block content, via [Theme Supports](/docs/how-to-guides/themes/theme-support.md), or by including a [theme.json](/docs/how-to-guides/themes/theme-json.md) file. A theme does not have to be a `Block` theme in order to take advantage of some of the flexibility provided by the use of a `theme.json` file. +In terms of block editor terminology this is any theme that defines its templates in the traditional `.php` file format, and that doesn't have an `index.html` format template in the `/block-templates` or `/templates` folders. A `Classic` theme has the ability to provide configuration and styling options to the block editor, and block content, via [Theme Supports](/docs/how-to-guides/themes/theme-support.md), or by including a [theme.json](/docs/how-to-guides/themes/global-settings-and-styles.md) file. A theme does not have to be a `Block` theme in order to take advantage of some of the flexibility provided by the use of a `theme.json` file. ### Block theme @@ -18,5 +18,5 @@ There isn't an FSE specific theme type. In WordPress > 5.9 FSE is enabled for an **Contents** -- [Global Settings (theme.json)](/docs/how-to-guides/themes/theme-json.md) +- [Global Settings (theme.json)](/docs/how-to-guides/themes/global-settings-and-styles.md) - [Theme Support](/docs/how-to-guides/themes/theme-support.md) diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/global-settings-and-styles.md similarity index 98% rename from docs/how-to-guides/themes/theme-json.md rename to docs/how-to-guides/themes/global-settings-and-styles.md index 1f7480649f6ab1..130b6271d13bdf 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/global-settings-and-styles.md @@ -2,32 +2,6 @@ WordPress 5.8 comes with [a new mechanism](https://make.wordpress.org/core/2021/06/25/introducing-theme-json-in-wordpress-5-8/) to configure the editor that enables a finer-grained control and introduces the first step in managing styles for future WordPress releases: the `theme.json` file. Then `theme.json` [evolved to a v2](https://make.wordpress.org/core/2022/01/08/updates-for-settings-styles-and-theme-json/) with WordPress 5.9 release. This page documents its format. -- Rationale - - Settings for the block editor - - Settings can be controlled per block - - Styles are managed - - CSS Custom Properties: presets & custom -- Specification - - version - - settings - - Backward compatibility with add_theme_support - - Presets - - Custom - - Setting examples - - styles - - Top-level - - Block-level - - Elements - - Variations - - customTemplates - - templateParts - - patterns -- FAQ - - The naming schema of CSS Custom Properties - - Why using -- as a separator? - - How settings under "custom" create new CSS Custom Properties - - Why does it take so long to update the styles in the browser? - ## Rationale The Block Editor API has evolved at different velocities and there are some growing pains, specially in areas that affect themes. Examples of this are: the ability to [control the editor programmatically](https://make.wordpress.org/core/2020/01/23/controlling-the-block-editor/), or [a block style system](https://github.com/WordPress/gutenberg/issues/9534) that facilitates user, theme, and core style preferences. diff --git a/docs/how-to-guides/themes/theme-support.md b/docs/how-to-guides/themes/theme-support.md index b978ede928b83d..88e69938737b7a 100644 --- a/docs/how-to-guides/themes/theme-support.md +++ b/docs/how-to-guides/themes/theme-support.md @@ -315,7 +315,7 @@ Themes can opt out of generated block layout styles that provide default structu add_theme_support( 'disable-layout-styles' ); ``` -For themes looking to customize `blockGap` styles or block spacing, see [the developer docs on Global Settings & Styles](/docs/how-to-guides/themes/theme-json/#what-is-blockgap-and-how-can-i-use-it). +For themes looking to customize `blockGap` styles or block spacing, see [the developer docs on Global Settings & Styles](/docs/how-to-guides/themes/global-settings-and-styles.md#what-is-blockgap-and-how-can-i-use-it). ### Supporting custom line heights @@ -434,7 +434,7 @@ add_theme_support( 'custom-spacing' ); ## Link color control -Link support has been made stable as part of WordPress 5.8. It's `false` by default and themes can enable it via the [theme.json file](./theme-json.md): +Link support has been made stable as part of WordPress 5.8. It's `false` by default and themes can enable it via the [theme.json file](/docs/how-to-guides/curating-the-editor-experience/theme-json.md): ```json { diff --git a/docs/manifest.json b/docs/manifest.json index 86a889406ce919..67b8fac99f7137 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -95,6 +95,12 @@ "markdown_source": "../docs/getting-started/fundamentals/markup-representation-block.md", "parent": "fundamentals" }, + { + "title": "Static or Dynamic rendering of a block", + "slug": "static-dynamic-rendering", + "markdown_source": "../docs/getting-started/fundamentals/static-dynamic-rendering.md", + "parent": "fundamentals" + }, { "title": "Working with Javascript for the Block Editor", "slug": "javascript-in-the-block-editor", @@ -212,9 +218,39 @@ { "title": "Curating the Editor Experience", "slug": "curating-the-editor-experience", - "markdown_source": "../docs/how-to-guides/curating-the-editor-experience.md", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/README.md", "parent": "how-to-guides" }, + { + "title": "Block Locking API", + "slug": "block-locking", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/block-locking.md", + "parent": "curating-the-editor-experience" + }, + { + "title": "Patterns", + "slug": "patterns", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/patterns.md", + "parent": "curating-the-editor-experience" + }, + { + "title": "theme.json", + "slug": "theme-json", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/theme-json.md", + "parent": "curating-the-editor-experience" + }, + { + "title": "Filters and hooks", + "slug": "filters-and-hooks", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md", + "parent": "curating-the-editor-experience" + }, + { + "title": "Disable Editor functionality", + "slug": "disable-editor-functionality", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md", + "parent": "curating-the-editor-experience" + }, { "title": "Enqueueing assets in the Editor", "slug": "enqueueing-assets-in-the-editor", @@ -271,8 +307,8 @@ }, { "title": "Global Settings & Styles (theme.json)", - "slug": "theme-json", - "markdown_source": "../docs/how-to-guides/themes/theme-json.md", + "slug": "global-settings-and-styles", + "markdown_source": "../docs/how-to-guides/themes/global-settings-and-styles.md", "parent": "themes" }, { diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index 7fd0e68c9bd8c0..4a59c34813448f 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -437,7 +437,7 @@ _**Note:** Since WordPress 6.2._ - Subproperties: - `minHeight`: type `boolean`, default value `false` -This value signals that a block supports some of the CSS style properties related to dimensions. When it does, the block editor will show UI controls for the user to set their values if [the theme declares support](/docs/how-to-guides/themes/theme-json/#opt-in-into-ui-controls). +This value signals that a block supports some of the CSS style properties related to dimensions. When it does, the block editor will show UI controls for the user to set their values if [the theme declares support](/docs/how-to-guides/themes/global-settings-and-styles.md#opt-in-into-ui-controls). ```js supports: { @@ -491,7 +491,7 @@ selectors: { The filter can be applied to an element inside the block by setting the `selectors.filter.duotone` selector. -Duotone presets are sourced from `color.duotone` in [theme.json](/docs/how-to-guides/themes/theme-json.md). +Duotone presets are sourced from `color.duotone` in [theme.json](/docs/how-to-guides/themes/global-settings-and-styles.md). When the block declares support for `filter.duotone`, the attributes definition is extended to include the attribute `style`: @@ -675,7 +675,7 @@ _**Note:** Since WordPress 6.2._ - Subproperties: - `sticky`: type `boolean`, default value `false` -This value signals that a block supports some of the CSS style properties related to position. When it does, the block editor will show UI controls for the user to set their values if [the theme declares support](/docs/how-to-guides/themes/theme-json/#opt-in-into-ui-controls). +This value signals that a block supports some of the CSS style properties related to position. When it does, the block editor will show UI controls for the user to set their values if [the theme declares support](/docs/how-to-guides/themes/global-settings-and-styles.md#opt-in-into-ui-controls). Note that sticky position controls are currently only available for blocks set at the root level of the document. Setting a block to the `sticky` position will stick the block to its most immediate parent when the user scrolls the page. diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 9f25ad0a594b89..c05cdd3eb009b1 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -333,7 +333,7 @@ Display multiple images in a rich gallery. ([Source](https://github.com/WordPres - **Name:** core/gallery - **Category:** media - **Supports:** align, anchor, color (background, gradients, ~~text~~), layout (default, ~~allowEditing~~, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin, padding), units (em, px, rem, vh, vw), ~~html~~ -- **Attributes:** allowResize, caption, columns, fixedHeight, ids, imageCrop, images, linkTarget, linkTo, shortCodeTransforms, sizeSlug +- **Attributes:** allowResize, caption, columns, fixedHeight, ids, imageCrop, images, linkTarget, linkTo, randomOrder, shortCodeTransforms, sizeSlug ## Group @@ -630,7 +630,7 @@ Display a post's featured image. ([Source](https://github.com/WordPress/gutenber - **Name:** core/post-featured-image - **Category:** theme - **Supports:** align (center, full, left, right, wide), color (~~background~~, ~~text~~), spacing (margin, padding), ~~html~~ -- **Attributes:** aspectRatio, customGradient, customOverlayColor, dimRatio, gradient, height, isLink, linkTarget, overlayColor, rel, scale, sizeSlug, width +- **Attributes:** aspectRatio, customGradient, customOverlayColor, dimRatio, gradient, height, isLink, linkTarget, overlayColor, rel, scale, sizeSlug, useFirstImageFromPost, width ## Post Navigation Link @@ -799,7 +799,7 @@ Help visitors find your content. ([Source](https://github.com/WordPress/gutenber - **Name:** core/search - **Category:** widgets - **Supports:** align (center, left, right), color (background, gradients, text), interactivity, typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** buttonBehavior, buttonPosition, buttonText, buttonUseIcon, isSearchFieldHidden, label, placeholder, query, showLabel, width, widthUnit +- **Attributes:** buttonPosition, buttonText, buttonUseIcon, isSearchFieldHidden, label, placeholder, query, showLabel, width, widthUnit ## Separator diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 38a93552bcbef2..7b0bd386daaf48 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -588,18 +588,6 @@ _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. @@ -1663,18 +1651,6 @@ _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/theme-json-reference/README.md b/docs/reference-guides/theme-json-reference/README.md index 92f6f77e298c00..11605b21625ad2 100644 --- a/docs/reference-guides/theme-json-reference/README.md +++ b/docs/reference-guides/theme-json-reference/README.md @@ -1,6 +1,6 @@ # Theme.json Reference -This reference guide lists the settings and style properties defined in the theme.json schema. See the [theme.json how to guide](/docs/how-to-guides/themes/theme-json.md) for examples and guide on how to use the theme.json file in your theme. +This reference guide lists the settings and style properties defined in the theme.json schema. See the [theme.json how to guide](/docs/how-to-guides/themes/global-settings-and-styles.md) for examples and guide on how to use the theme.json file in your theme. - [Version 2 (living reference)](/docs/reference-guides/theme-json-reference/theme-json-living.md) diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 627fee6071816f..4baa5a6009ded6 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -6,7 +6,7 @@ > - the [theme.json v1](/docs/reference-guides/theme-json-reference/theme-json-v1.md) specification, and > - the [reference to migrate from theme.json v1 to v2](/docs/reference-guides/theme-json-reference/theme-json-migrations.md). -This reference guide lists the settings and style properties defined in the `theme.json` schema. See the [theme.json how to guide](/docs/how-to-guides/themes/theme-json.md) for examples and guidance on how to use the `theme.json` file in your theme. +This reference guide lists the settings and style properties defined in the `theme.json` schema. See the [theme.json how to guide](/docs/how-to-guides/themes/global-settings-and-styles.md) for examples and guidance on how to use the `theme.json` file in your theme. ## Schema diff --git a/docs/toc.json b/docs/toc.json index 961fc88fae4f52..849de991c78080 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -42,6 +42,9 @@ { "docs/getting-started/fundamentals/markup-representation-block.md": [] }, + { + "docs/getting-started/fundamentals/static-dynamic-rendering.md": [] + }, { "docs/getting-started/fundamentals/javascript-in-the-block-editor.md": [] } @@ -99,7 +102,25 @@ } ] }, - { "docs/how-to-guides/curating-the-editor-experience.md": [] }, + { + "docs/how-to-guides/curating-the-editor-experience/README.md": [ + { + "docs/how-to-guides/curating-the-editor-experience/block-locking.md": [] + }, + { + "docs/how-to-guides/curating-the-editor-experience/patterns.md": [] + }, + { + "docs/how-to-guides/curating-the-editor-experience/theme-json.md": [] + }, + { + "docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md": [] + }, + { + "docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md": [] + } + ] + }, { "docs/how-to-guides/enqueueing-assets-in-the-editor.md": [] }, { "docs/how-to-guides/feature-flags.md": [] }, { "docs/how-to-guides/format-api.md": [] }, @@ -112,7 +133,9 @@ { "docs/how-to-guides/propagating-updates.md": [] }, { "docs/how-to-guides/themes/README.md": [ - { "docs/how-to-guides/themes/theme-json.md": [] }, + { + "docs/how-to-guides/themes/global-settings-and-styles.md": [] + }, { "docs/how-to-guides/themes/theme-support.md": [] } ] }, diff --git a/gutenberg.php b/gutenberg.php index 406dae417b3aa3..9559f838608da9 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.3 * Requires PHP: 7.0 - * Version: 17.4.0-rc.1 + * Version: 17.5.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php index c43603046c0c08..4b5f5614d64c9f 100644 --- a/lib/block-supports/background.php +++ b/lib/block-supports/background.php @@ -96,6 +96,7 @@ function gutenberg_render_background_support( $block_content, $block ) { $updated_style .= $styles['css']; $tags->set_attribute( 'style', $updated_style ); + $tags->add_class( 'has-background' ); } return $tags->get_updated_html(); diff --git a/lib/block-supports/behaviors.php b/lib/block-supports/behaviors.php deleted file mode 100644 index 383f4b4e6f9279..00000000000000 --- a/lib/block-supports/behaviors.php +++ /dev/null @@ -1,260 +0,0 @@ -attributes ) { - $block_type->attributes = array(); - } - - $block_type->attributes['behaviors'] = array( - 'type' => 'object', - ); - - // If it supports the lightbox behavior, add the hook to that block. - // In the future, this should be a loop with all the behaviors. - $has_lightbox_support = block_has_support( $block_type, array( 'behaviors', 'lightbox' ), false ); - if ( $has_lightbox_support ) { - // Use priority 15 to run this hook after other hooks/plugins. - // They could use the `render_block_{$this->name}` filter to modify the markup. - add_filter( 'render_block_' . $block_type->name, 'gutenberg_render_behaviors_support_lightbox', 15, 2 ); - } -} - -/** - * Add the directives and layout needed for the lightbox behavior. - * - * @param string $block_content Rendered block content. - * @param array $block Block object. - * @return string Filtered block content. - */ -function gutenberg_render_behaviors_support_lightbox( $block_content, $block ) { - - // We've deprecated the lightbox implementation via behaviors. - // While we may continue to explore behaviors in the future, the lightbox - // logic seems very specific to the image and will likely never be a part - // of behaviors, even in the future. With that in mind, we've rewritten the lightbox - // to be a feature of the image block and will also soon remove the block_supports. - // *Note: This logic for generating the lightbox markup has been duplicated and moved - // to the image block's index.php.* - // See https://github.com/WordPress/gutenberg/issues/53403. - _deprecated_function( 'gutenberg_render_behaviors_support_lightbox', 'Gutenberg 17.0.0', '' ); - - $link_destination = isset( $block['attrs']['linkDestination'] ) ? $block['attrs']['linkDestination'] : 'none'; - // Get the lightbox setting from the block attributes. - if ( isset( $block['attrs']['behaviors']['lightbox'] ) ) { - $lightbox_settings = $block['attrs']['behaviors']['lightbox']; - // If the lightbox setting is not set in the block attributes, get it from the theme.json file. - } else { - $theme_data = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data()->get_data(); - if ( isset( $theme_data['behaviors']['blocks'][ $block['blockName'] ]['lightbox'] ) ) { - $lightbox_settings = $theme_data['behaviors']['blocks'][ $block['blockName'] ]['lightbox']; - } else { - $lightbox_settings = null; - } - } - - if ( isset( $lightbox_settings['enabled'] ) && false === $lightbox_settings['enabled'] ) { - return $block_content; - } - - if ( ! $lightbox_settings || 'none' !== $link_destination ) { - return $block_content; - } - - $processor = new WP_HTML_Tag_Processor( $block_content ); - - $aria_label = __( 'Enlarge image', 'gutenberg' ); - - $processor->next_tag( 'img' ); - $alt_attribute = $processor->get_attribute( 'alt' ); - - // An empty alt attribute `alt=""` is valid for decorative images. - if ( is_string( $alt_attribute ) ) { - $alt_attribute = trim( $alt_attribute ); - } - - // It only makes sense to append the alt text to the button aria-label when the alt text is non-empty. - if ( $alt_attribute ) { - /* translators: %s: Image alt text. */ - $aria_label = sprintf( __( 'Enlarge image: %s', 'gutenberg' ), $alt_attribute ); - } - - // If we don't set a default, it won't work if Lightbox is set to enabled by default. - $lightbox_animation = 'zoom'; - if ( isset( $lightbox_settings['animation'] ) && '' !== $lightbox_settings['animation'] ) { - $lightbox_animation = $lightbox_settings['animation']; - } - - // Note: We want to store the `src` in the context so we - // can set it dynamically when the lightbox is opened. - if ( isset( $block['attrs']['id'] ) ) { - $img_uploaded_src = wp_get_attachment_url( $block['attrs']['id'] ); - $img_metadata = wp_get_attachment_metadata( $block['attrs']['id'] ); - $img_width = $img_metadata['width'] ?? 'none'; - $img_height = $img_metadata['height'] ?? 'none'; - } else { - $img_uploaded_src = $processor->get_attribute( 'src' ); - $img_width = 'none'; - $img_height = 'none'; - } - - if ( isset( $block['attrs']['scale'] ) ) { - $scale_attr = $block['attrs']['scale']; - } else { - $scale_attr = false; - } - - $w = new WP_HTML_Tag_Processor( $block_content ); - $w->next_tag( 'figure' ); - $w->add_class( 'wp-lightbox-container' ); - $w->set_attribute( 'data-wp-interactive', true ); - - $w->set_attribute( - 'data-wp-context', - sprintf( - '{ "core": - { "image": - { "imageLoaded": false, - "initialized": false, - "lightboxEnabled": false, - "hideAnimationEnabled": false, - "preloadInitialized": false, - "lightboxAnimation": "%s", - "imageUploadedSrc": "%s", - "imageCurrentSrc": "", - "targetWidth": "%s", - "targetHeight": "%s", - "scaleAttr": "%s" - } - } - }', - $lightbox_animation, - $img_uploaded_src, - $img_width, - $img_height, - $scale_attr - ) - ); - $w->next_tag( 'img' ); - $w->set_attribute( 'data-wp-init', 'effects.core.image.setCurrentSrc' ); - $w->set_attribute( 'data-wp-on--load', 'actions.core.image.handleLoad' ); - $w->set_attribute( 'data-wp-effect', 'effects.core.image.setButtonStyles' ); - $body_content = $w->get_updated_html(); - - // Wrap the image in the body content with a button. - $img = null; - preg_match( '/]+>/', $body_content, $img ); - - $button = - $img[0] - . ''; - - $body_content = preg_replace( '/]+>/', $button, $body_content ); - - // We need both a responsive image and an enlarged image to animate - // the zoom seamlessly on slow internet connections; the responsive - // image is a copy of the one in the body, which animates immediately - // as the lightbox is opened, while the enlarged one is a full-sized - // version that will likely still be loading as the animation begins. - $m = new WP_HTML_Tag_Processor( $block_content ); - $m->next_tag( 'figure' ); - $m->add_class( 'responsive-image' ); - $m->next_tag( 'img' ); - // We want to set the 'src' attribute to an empty string in the responsive image - // because otherwise, as of this writing, the wp_filter_content_tags() function in - // WordPress will automatically add a 'srcset' attribute to the image, which will at - // times cause the incorrectly sized image to be loaded in the lightbox on Firefox. - // Because of this, we bind the 'src' attribute explicitly the current src to reliably - // use the exact same image as in the content when the lightbox is first opened while - // we wait for the larger image to load. - $m->set_attribute( 'src', '' ); - $m->set_attribute( 'data-wp-bind--src', 'context.core.image.imageCurrentSrc' ); - $m->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); - $initial_image_content = $m->get_updated_html(); - - $q = new WP_HTML_Tag_Processor( $block_content ); - $q->next_tag( 'figure' ); - $q->add_class( 'enlarged-image' ); - $q->next_tag( 'img' ); - - // We set the 'src' attribute to an empty string to prevent the browser from loading the image - // on initial page load, then bind the attribute to a selector that returns the full-sized image src when - // the lightbox is opened. We could use 'loading=lazy' in combination with the 'hidden' attribute to - // accomplish the same behavior, but that approach breaks progressive loading of the image in Safari - // and Chrome (see https://github.com/WordPress/gutenberg/pull/52765#issuecomment-1674008151). Until that - // is resolved, manually setting the 'src' seems to be the best solution to load the large image on demand. - $q->set_attribute( 'src', '' ); - $q->set_attribute( 'data-wp-bind--src', 'selectors.core.image.enlargedImgSrc' ); - $q->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); - $enlarged_image_content = $q->get_updated_html(); - - $background_color = esc_attr( wp_get_global_styles( array( 'color', 'background' ) ) ); - - $close_button_icon = ''; - $close_button_color = esc_attr( wp_get_global_styles( array( 'color', 'text' ) ) ); - $dialog_label = esc_attr__( 'Enlarged image', 'gutenberg' ); - $close_button_label = esc_attr__( 'Close', 'gutenberg' ); - - $lightbox_html = << - - - -
-
-HTML; - - return str_replace( '', $lightbox_html . '', $body_content ); -} - -// Register the block support. -WP_Block_Supports::get_instance()->register( - 'behaviors', - array( - 'register_attribute' => 'gutenberg_register_behaviors_support', - ) -); diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index db02364b707901..6215fbb74caf2f 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -639,7 +639,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { * for features like the enhanced pagination of the Query block. */ $container_class = gutenberg_incremental_id_per_prefix( - 'wp-container-' . sanitize_title( $block['blockName'] ) . '-layout-' + 'wp-container-' . sanitize_title( $block['blockName'] ) . '-is-layout-' ); // Set the correct layout type for blocks using legacy content width. @@ -893,7 +893,7 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) { if ( $classes ) { $classes = explode( ' ', $classes ); foreach ( $classes as $class_name ) { - if ( str_contains( $class_name, 'layout' ) ) { + if ( str_contains( $class_name, 'is-layout-' ) ) { array_push( $layout_classes, $class_name ); $processor->remove_class( $class_name ); } diff --git a/lib/blocks.php b/lib/blocks.php index 698b5d6873748f..d5283afeb7f999 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -439,33 +439,6 @@ function gutenberg_legacy_wp_block_post_meta( $value, $object_id, $meta_key, $si add_filter( 'default_post_metadata', 'gutenberg_legacy_wp_block_post_meta', 10, 4 ); -/** - * Complements the lightbox implementation for the 'core/image' block. - * - * This function is INTENTIONALLY left out of core as it only provides - * backwards compatibility for the legacy lightbox syntax that was only - * introduced in Gutenberg. The legacy syntax was using the `behaviors` key in - * the block attrbutes and the `theme.json` file. - * - * @since 16.7.0 - * - * @param array $block The block to check. - * @return array The block with the legacyLightboxSettings set if available. - */ -function gutenberg_should_render_lightbox( $block ) { - - if ( 'core/image' !== $block['blockName'] ) { - return $block; - } - - if ( isset( $block['attrs']['behaviors']['lightbox'] ) ) { - $block['legacyLightboxSettings'] = $block['attrs']['behaviors']['lightbox']; - } - - return $block; -} - -add_filter( 'render_block_data', 'gutenberg_should_render_lightbox', 15, 1 ); /** * Registers the metadata block attribute for all block types. diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 4fe5a3a3bf6a88..aa8de83df9597b 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -697,7 +697,7 @@ public function __construct( $theme_json = array(), $origin = 'theme' ) { $origin = 'theme'; } - $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json ); + $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); $registry = WP_Block_Type_Registry::get_instance(); $valid_block_names = array_keys( $registry->get_all_registered() ); $valid_element_names = array_keys( static::ELEMENTS ); @@ -1021,8 +1021,7 @@ protected static function get_blocks_metadata() { if ( ! empty( $block_type->styles ) ) { $style_selectors = array(); foreach ( $block_type->styles as $style ) { - // The style variation classname is duplicated in the selector to ensure that it overrides core block styles. - $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'] . '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); + $style_selectors[ $style['name'] ] = static::append_to_selector( '.is-style-' . $style['name'], static::$blocks_metadata[ $block_name ]['selector'] ); } static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors; } @@ -2991,7 +2990,7 @@ protected static function filter_slugs( $node, $slugs ) { public static function remove_insecure_properties( $theme_json ) { $sanitized = array(); - $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json ); + $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); $valid_block_names = array_keys( static::get_blocks_metadata() ); $valid_element_names = array_keys( static::ELEMENTS ); diff --git a/lib/class-wp-theme-json-schema-gutenberg.php b/lib/class-wp-theme-json-schema-gutenberg.php deleted file mode 100644 index d11545751af362..00000000000000 --- a/lib/class-wp-theme-json-schema-gutenberg.php +++ /dev/null @@ -1,202 +0,0 @@ - 'border.radius', - 'spacing.customMargin' => 'spacing.margin', - 'spacing.customPadding' => 'spacing.padding', - 'typography.customLineHeight' => 'typography.lineHeight', - ); - - /** - * Function that migrates a given theme.json structure to the last version. - * - * @since 5.9.0 - * - * @param array $theme_json The structure to migrate. - * - * @return array The structure in the last version. - */ - public static function migrate( $theme_json ) { - if ( ! isset( $theme_json['version'] ) ) { - $theme_json = array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - ); - } - - if ( 1 === $theme_json['version'] ) { - $theme_json = self::migrate_v1_to_v2( $theme_json ); - } - - if ( 2 === $theme_json['version'] ) { - $theme_json = self::migrate_deprecated_lightbox_behaviors( $theme_json ); - } - - return $theme_json; - } - - /** - * Removes the custom prefixes for a few properties - * that were part of v1: - * - * 'border.customRadius' => 'border.radius', - * 'spacing.customMargin' => 'spacing.margin', - * 'spacing.customPadding' => 'spacing.padding', - * 'typography.customLineHeight' => 'typography.lineHeight', - * - * @since 5.9.0 - * - * @param array $old Data to migrate. - * - * @return array Data without the custom prefixes. - */ - private static function migrate_v1_to_v2( $old ) { - // Copy everything. - $new = $old; - - // Overwrite the things that changed. - if ( isset( $old['settings'] ) ) { - $new['settings'] = self::rename_paths( $old['settings'], self::V1_TO_V2_RENAMED_PATHS ); - } - - // Set the new version. - $new['version'] = 2; - - return $new; - } - - - /** - * Migrate away from the previous syntax that used a top-level "behaviors" key - * in the `theme.json` to a new "lightbox" setting. - * - * This function SHOULD NOT be ported to Core!!! - * - * It is a temporary migration that will be removed in Gutenberg 17.0.0 - * - * @since 16.7.0 - * - * @param array $old Data with (potentially) behaviors. - * @return array Data with behaviors removed. - */ - private static function migrate_deprecated_lightbox_behaviors( $old ) { - // Copy everything. - $new = $old; - - // Migrate the old behaviors syntax to the new "lightbox" syntax. - if ( isset( $old['behaviors']['blocks']['core/image']['lightbox']['enabled'] ) ) { - _wp_array_set( - $new, - array( 'settings', 'blocks', 'core/image', 'lightbox', 'enabled' ), - $old['behaviors']['blocks']['core/image']['lightbox']['enabled'] - ); - } - - // Migrate the behaviors setting to the new syntax. This setting controls - // whether the Lightbox UI shows up in the block editor. - if ( isset( $old['settings']['blocks']['core/image']['behaviors']['lightbox'] ) ) { - _wp_array_set( - $new, - array( 'settings', 'blocks', 'core/image', 'lightbox', 'allowEditing' ), - $old['settings']['blocks']['core/image']['behaviors']['lightbox'] - ); - } - - return $new; - } - - /** - * Processes the settings subtree. - * - * @since 5.9.0 - * - * @param array $settings Array to process. - * @param array $paths_to_rename Paths to rename. - * - * @return array The settings in the new format. - */ - private static function rename_paths( $settings, $paths_to_rename ) { - $new_settings = $settings; - - // Process any renamed/moved paths within default settings. - self::rename_settings( $new_settings, $paths_to_rename ); - - // Process individual block settings. - if ( isset( $new_settings['blocks'] ) && is_array( $new_settings['blocks'] ) ) { - foreach ( $new_settings['blocks'] as &$block_settings ) { - self::rename_settings( $block_settings, $paths_to_rename ); - } - } - - return $new_settings; - } - - /** - * Processes a settings array, renaming or moving properties. - * - * @since 5.9.0 - * - * @param array $settings Reference to settings either defaults or an individual block's. - * @param array $paths_to_rename Paths to rename. - */ - private static function rename_settings( &$settings, $paths_to_rename ) { - foreach ( $paths_to_rename as $original => $renamed ) { - $original_path = explode( '.', $original ); - $renamed_path = explode( '.', $renamed ); - $current_value = _wp_array_get( $settings, $original_path, null ); - - if ( null !== $current_value ) { - _wp_array_set( $settings, $renamed_path, $current_value ); - self::unset_setting_by_path( $settings, $original_path ); - } - } - } - - /** - * Removes a property from within the provided settings by its path. - * - * @since 5.9.0 - * - * @param array $settings Reference to the current settings array. - * @param array $path Path to the property to be removed. - */ - private static function unset_setting_by_path( &$settings, $path ) { - $tmp_settings = &$settings; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - $last_key = array_pop( $path ); - foreach ( $path as $key ) { - $tmp_settings = &$tmp_settings[ $key ]; - } - - unset( $tmp_settings[ $last_key ] ); - } -} 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 index 189e5a695c23a7..9c270f59fa220e 100644 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -361,16 +361,25 @@ private static function get_classes( $attributes ) { $text_decoration = $attributes['style']['typography']['textDecoration'] ?? null; $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration ); + // Sets the is-collapsed class when the navigation is set to always use the overlay. + // This saves us from needing to do this check in the view.js file (see the collapseNav function). + $is_collapsed_class = static::is_always_overlay( $attributes ) ? array( 'is-collapsed' ) : array(); + $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() + $text_decoration ? array( $text_decoration_class ) : array(), + $is_collapsed_class ); return implode( ' ', $classes ); } + private static function is_always_overlay( $attributes ) { + return isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; + } + /** * Get styles for the navigation block. * @@ -397,16 +406,12 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $colors = gutenberg_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']; @@ -428,11 +433,11 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $responsive_dialog_directives = ''; $close_button_directives = ''; if ( $should_load_view_script ) { - $open_button_directives = ' + $open_button_directives = ' data-wp-on--click="actions.openMenuOnClick" data-wp-on--keydown="actions.handleMenuKeydown" '; - $responsive_container_directives = ' + $responsive_container_directives = ' data-wp-class--has-modal-open="state.isMenuOpen" data-wp-class--is-menu-open="state.isMenuOpen" data-wp-watch="callbacks.initMenu" @@ -440,15 +445,17 @@ private static function get_responsive_container_markup( $attributes, $inner_blo data-wp-on--focusout="actions.handleMenuFocusout" tabindex="-1" '; - $responsive_dialog_directives = ' + $responsive_dialog_directives = ' data-wp-bind--aria-modal="state.ariaModal" data-wp-bind--aria-label="state.ariaLabel" data-wp-bind--role="state.roleAttribute" - data-wp-watch="callbacks.focusFirstElement" '; - $close_button_directives = ' + $close_button_directives = ' data-wp-on--click="actions.closeMenuOnClick" '; + $responsive_container_content_directives = ' + data-wp-watch="callbacks.focusFirstElement" + '; } return sprintf( @@ -457,7 +464,7 @@ private static function get_responsive_container_markup( $attributes, $inner_blo
-
+
%2$s
@@ -475,7 +482,8 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $open_button_directives, $responsive_container_directives, $responsive_dialog_directives, - $close_button_directives + $close_button_directives, + $responsive_container_content_directives ); } @@ -501,7 +509,7 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) ); if ( $is_responsive_menu ) { - $nav_element_directives = static::get_nav_element_directives( $should_load_view_script ); + $nav_element_directives = static::get_nav_element_directives( $should_load_view_script, $attributes ); $wrapper_attributes .= ' ' . $nav_element_directives; } @@ -514,12 +522,12 @@ private static function get_nav_wrapper_attributes( $attributes, $inner_blocks ) * @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 ) { + private static function get_nav_element_directives( $should_load_view_script, $attributes ) { if ( ! $should_load_view_script ) { return ''; } // When adding to this array be mindful of security concerns. - $nav_element_context = wp_json_encode( + $nav_element_context = wp_json_encode( array( 'overlayOpenedBy' => array(), 'type' => 'overlay', @@ -528,10 +536,20 @@ private static function get_nav_element_directives( $should_load_view_script ) { ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP ); - return ' + $nav_element_directives = ' data-wp-interactive=\'{"namespace":"core/navigation"}\' data-wp-context=\'' . $nav_element_context . '\' '; + + // When the navigation overlayMenu attribute is set to "always" + // we don't need to use JavaScript to collapse the menu as we set the class manually. + if ( ! static::is_always_overlay( $attributes ) ) { + $nav_element_directives .= 'data-wp-init="callbacks.initNav"'; + $nav_element_directives .= ' '; // space separator + $nav_element_directives .= 'data-wp-class--is-collapsed="context.isCollapsed"'; + } + + return $nav_element_directives; } /** diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 88e46b478389d2..d4bb6c9b4586eb 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -150,7 +150,7 @@ function gutenberg_render_block_connections( $block_content, $block, $block_inst continue; } - $custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance ); + $custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance, $attribute_name ); } else { // If the attribute does not specify the name of the custom field, skip it. if ( ! isset( $attribute_value['value'] ) ) { diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php index bf89ba177b6e94..4f9e06cb13b945 100644 --- a/lib/experimental/connection-sources/index.php +++ b/lib/experimental/connection-sources/index.php @@ -12,8 +12,12 @@ // if it doesn't, `get_post_meta()` will just return an empty string. return get_post_meta( $block_instance->context['postId'], $meta_field, true ); }, - 'pattern_attributes' => function ( $block_instance ) { + 'pattern_attributes' => function ( $block_instance, $attribute_name ) { $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id ), false ); + return _wp_array_get( + $block_instance->context, + array( 'pattern/overrides', $block_id, $attribute_name ), + false + ); }, ); diff --git a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php index 7d954e79e96a3c..35e6856e50aad8 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php @@ -76,21 +76,6 @@ public static function merge_fonts_data( $font1, $font2 ) { return $merged_font; } - /** - * Returns whether the given file has a font MIME type. - * - * @since 6.5.0 - * - * @param string $filepath The file to check. - * @return bool True if the file has a font MIME type, false otherwise. - */ - public static function has_font_mime_type( $filepath ) { - $allowed_mime_types = WP_Font_Library::get_expected_font_mime_types_per_php_version(); - $filetype = wp_check_filetype( $filepath, $allowed_mime_types ); - - return in_array( $filetype['type'], $allowed_mime_types, true ); - } - /** * Format font family to make it valid CSS. * 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 58d4f476e834d1..a4204dfe1fa2c7 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family.php @@ -202,11 +202,6 @@ private function get_upload_overrides( $filename ) { * False if the download failed. */ private function download_asset( $url, $filename ) { - // Checks if the file to be downloaded has a font mime type. - if ( ! WP_Font_Family_Utils::has_font_mime_type( $filename ) ) { - return false; - } - // Include file with download_url() if function doesn't exist. if ( ! function_exists( 'download_url' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; @@ -263,12 +258,6 @@ private function move_font_face_asset( $font_face, $file ) { // because it is no longer needed. unset( $new_font_face['uploadedFile'] ); - // If the filename has no font mime type, don't move the file and - // return the font face definition without src to be ignored later. - if ( ! WP_Font_Family_Utils::has_font_mime_type( $filename ) ) { - return $new_font_face; - } - // Move the uploaded font asset from the temp folder to the fonts directory. if ( ! function_exists( 'wp_handle_upload' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/experimental/fonts/font-library/class-wp-font-library.php index 9320a554e510c7..59ec5e93fa787e 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-library.php +++ b/lib/experimental/fonts/font-library/class-wp-font-library.php @@ -63,15 +63,57 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio */ public static function register_font_collection( $config ) { $new_collection = new WP_Font_Collection( $config ); - - if ( isset( self::$collections[ $config['id'] ] ) ) { - return new WP_Error( 'font_collection_registration_error', 'Font collection already registered.' ); + if ( self::is_collection_registered( $config['id'] ) ) { + $error_message = sprintf( + /* translators: %s: Font collection id. */ + __( 'Font collection with id: "%s" is already registered.', 'default' ), + $config['id'] + ); + _doing_it_wrong( + __METHOD__, + $error_message, + '6.5.0' + ); + return new WP_Error( 'font_collection_registration_error', $error_message ); } - self::$collections[ $config['id'] ] = $new_collection; return $new_collection; } + /** + * Unregisters a previously registered font collection. + * + * @since 6.5.0 + * + * @param string $collection_id Font collection ID. + * @return bool True if the font collection was unregistered successfully and false otherwise. + */ + public static function unregister_font_collection( $collection_id ) { + if ( ! self::is_collection_registered( $collection_id ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Font collection id. */ + sprintf( __( 'Font collection "%s" not found.', 'default' ), $collection_id ), + '6.5.0' + ); + return false; + } + unset( self::$collections[ $collection_id ] ); + return true; + } + + /** + * Checks if a font collection is registered. + * + * @since 6.5.0 + * + * @param string $collection_id Font collection ID. + * @return bool True if the font collection is registered and false otherwise. + */ + private static function is_collection_registered( $collection_id ) { + return array_key_exists( $collection_id, self::$collections ); + } + /** * Gets all the font collections available. * diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index c92a0d2697f315..0147d80b7bde94 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -44,8 +44,7 @@ public function register_routes() { array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), - 'permission_callback' => function () { - return true;}, + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), ), ) ); @@ -59,7 +58,7 @@ public function register_routes() { 'callback' => array( $this, 'install_fonts' ), 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), 'args' => array( - 'font_families' => array( + 'font_family_settings' => array( 'required' => true, 'type' => 'string', 'validate_callback' => array( $this, 'validate_install_font_families' ), @@ -92,85 +91,61 @@ public function register_routes() { * @param array $files Files to install. * @return array $error_messages Array of error messages. */ - private function get_validation_errors( $font_families, $files ) { + private function get_validation_errors( $font_family_settings, $files ) { $error_messages = array(); - if ( ! is_array( $font_families ) ) { - $error_messages[] = __( 'font_families should be an array of font families.', 'gutenberg' ); + if ( ! is_array( $font_family_settings ) ) { + $error_messages[] = __( 'font_family_settings should be a font family definition.', 'gutenberg' ); return $error_messages; } - // Checks if there is at least one font family. - if ( count( $font_families ) < 1 ) { - $error_messages[] = __( 'font_families should have at least one font family definition.', 'gutenberg' ); + if ( + ! isset( $font_family_settings['slug'] ) || + ! isset( $font_family_settings['name'] ) || + ! isset( $font_family_settings['fontFamily'] ) + ) { + $error_messages[] = __( 'Font family should have slug, name and fontFamily properties defined.', 'gutenberg' ); + return $error_messages; } - for ( $family_index = 0; $family_index < count( $font_families ); $family_index++ ) { - $font_family = $font_families[ $family_index ]; - - if ( - ! isset( $font_family['slug'] ) || - ! isset( $font_family['name'] ) || - ! isset( $font_family['fontFamily'] ) - ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have slug, name and fontFamily properties defined.', 'gutenberg' ), - $family_index - ); + if ( isset( $font_family_settings['fontFace'] ) ) { + if ( ! is_array( $font_family_settings['fontFace'] ) ) { + $error_messages[] = __( 'Font family should have fontFace property defined as an array.', 'gutenberg' ); } - if ( isset( $font_family['fontFace'] ) ) { - if ( ! is_array( $font_family['fontFace'] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have fontFace property defined as an array.', 'gutenberg' ), - $family_index - ); - continue; - } + if ( count( $font_family_settings['fontFace'] ) < 1 ) { + $error_messages[] = __( 'Font family should have at least one font face definition.', 'gutenberg' ); + } - if ( count( $font_family['fontFace'] ) < 1 ) { - $error_messages[] = sprintf( - // translators: 1: font family index. - __( 'Font family [%s] should have at least one font face definition.', 'gutenberg' ), - $family_index - ); - } + if ( ! empty( $font_family_settings['fontFace'] ) ) { + for ( $face_index = 0; $face_index < count( $font_family_settings['fontFace'] ); $face_index++ ) { - if ( ! empty( $font_family['fontFace'] ) ) { - for ( $face_index = 0; $face_index < count( $font_family['fontFace'] ); $face_index++ ) { + $font_face = $font_family_settings['fontFace'][ $face_index ]; + if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { + $error_messages[] = sprintf( + // translators: font face index. + __( 'Font family Font face [%1$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ), + $face_index + ); + } - $font_face = $font_family['fontFace'][ $face_index ]; - if ( ! isset( $font_face['fontWeight'] ) || ! isset( $font_face['fontStyle'] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] should have fontWeight and fontStyle properties defined.', 'gutenberg' ), - $family_index, - $face_index - ); - } + if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { + $error_messages[] = sprintf( + // translators: font face index. + __( 'Font family Font face [%1$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ), + $face_index + ); + } - if ( isset( $font_face['downloadFromUrl'] ) && isset( $font_face['uploadedFile'] ) ) { + if ( isset( $font_face['uploadedFile'] ) ) { + if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] should have only one of the downloadFromUrl or uploadedFile properties defined and not both.', 'gutenberg' ), - $family_index, + // translators: font face index. + __( 'Font family Font face [%1$s] file is not defined in the request files.', 'gutenberg' ), $face_index ); } - - if ( isset( $font_face['uploadedFile'] ) ) { - if ( ! isset( $files[ $font_face['uploadedFile'] ] ) ) { - $error_messages[] = sprintf( - // translators: 1: font family index, 2: font face index. - __( 'Font family [%1$s] Font face [%2$s] file is not defined in the request files.', 'gutenberg' ), - $family_index, - $face_index - ); - } - } } } } @@ -189,9 +164,9 @@ private function get_validation_errors( $font_families, $files ) { * @return true|WP_Error True if the parameter is valid, WP_Error otherwise. */ public function validate_install_font_families( $param, $request ) { - $font_families = json_decode( $param, true ); - $files = $request->get_file_params(); - $error_messages = $this->get_validation_errors( $font_families, $files ); + $font_family_settings = json_decode( $param, true ); + $files = $request->get_file_params(); + $error_messages = $this->get_validation_errors( $font_family_settings, $files ); if ( empty( $error_messages ) ) { return true; @@ -327,17 +302,15 @@ private function has_write_permission() { * * @since 6.5.0 * - * @param array[] $font_families Font families to install. + * @param array[] $font_family_settings Font family definition. * @return bool Whether the request needs write permissions. */ - private function needs_write_permission( $font_families ) { - foreach ( $font_families as $font ) { - if ( isset( $font['fontFace'] ) ) { - foreach ( $font['fontFace'] as $face ) { - // If the font is being downloaded from a URL or uploaded, it needs write permissions. - if ( isset( $face['downloadFromUrl'] ) || isset( $face['uploadedFile'] ) ) { - return true; - } + private function needs_write_permission( $font_family_settings ) { + if ( isset( $font_family_settings['fontFace'] ) ) { + foreach ( $font_family_settings['fontFace'] as $face ) { + // If the font is being downloaded from a URL or uploaded, it needs write permissions. + if ( isset( $face['downloadFromUrl'] ) || isset( $face['uploadedFile'] ) ) { + return true; } } } @@ -358,20 +331,20 @@ private function needs_write_permission( $font_families ) { */ public function install_fonts( $request ) { // Get new fonts to install. - $fonts_param = $request->get_param( 'font_families' ); + $font_family_settings = $request->get_param( 'font_family_settings' ); /* * As this is receiving form data, the font families are encoded as a string. * The form data is used because local fonts need to use that format to * attach the files in the request. */ - $fonts_to_install = json_decode( $fonts_param, true ); + $font_family_settings = json_decode( $font_family_settings, true ); $successes = array(); $errors = array(); $response_status = 200; - if ( empty( $fonts_to_install ) ) { + if ( empty( $font_family_settings ) ) { $errors[] = new WP_Error( 'no_fonts_to_install', __( 'No fonts to install', 'gutenberg' ) @@ -379,7 +352,7 @@ public function install_fonts( $request ) { $response_status = 400; } - if ( $this->needs_write_permission( $fonts_to_install ) ) { + if ( $this->needs_write_permission( $font_family_settings ) ) { $upload_dir = WP_Font_Library::get_fonts_dir(); if ( ! $this->has_upload_directory() ) { if ( ! wp_mkdir_p( $upload_dir ) ) { @@ -415,15 +388,13 @@ public function install_fonts( $request ) { } // Get uploaded files (used when installing local fonts). - $files = $request->get_file_params(); - foreach ( $fonts_to_install as $font_data ) { - $font = new WP_Font_Family( $font_data ); - $result = $font->install( $files ); - if ( is_wp_error( $result ) ) { - $errors[] = $result; - } else { - $successes[] = $result; - } + $files = $request->get_file_params(); + $font = new WP_Font_Family( $font_family_settings ); + $result = $font->install( $files ); + if ( is_wp_error( $result ) ) { + $errors[] = $result; + } else { + $successes[] = $result; } $data = array( diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index 709f63e9126cbc..711a6bb40c282b 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -60,6 +60,19 @@ function wp_register_font_collection( $config ) { } } +if ( ! function_exists( 'wp_unregister_font_collection' ) ) { + /** + * Unregisters a font collection from the Font Library. + * + * @since 6.5.0 + * + * @param string $collection_id The font collection ID. + */ + function wp_unregister_font_collection( $collection_id ) { + WP_Font_Library::unregister_font_collection( $collection_id ); + } + +} $default_font_collection = array( 'id' => 'default-font-collection', diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index cf55a048bb9fa5..3b8a38f973815d 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -13,65 +13,91 @@ /** * This processor is built on top of the HTML Tag Processor and augments its * capabilities to process the Interactivity API directives. - * - * IMPORTANT DISCLAIMER: This code is highly experimental and its only purpose - * is to provide a way to test the server-side rendering of the Interactivity - * API. Most of this code will be discarded once the HTML Processor is - * available. Please restrain from investing unnecessary time and effort trying - * to improve this code. */ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 { + /** + * String containing the current root block. + * + * @var string + */ + public static $root_block = null; /** - * An array of root blocks. + * Array containing the direct children of interactive blocks. * * @var array */ - public static $root_block = null; + public static $children_of_interactive_block = array(); /** - * Add a root block to the variable. + * Sets the current root block. * * @param array $block The block to add. - * - * @return void */ public static function mark_root_block( $block ) { - self::$root_block = md5( serialize( $block ) ); + if ( null !== $block['blockName'] ) { + self::$root_block = $block['blockName'] . md5( serialize( $block ) ); + } else { + self::$root_block = md5( serialize( $block ) ); + } } /** - * Remove a root block to the variable. - * - * @return void + * Resets the root block. */ public static function unmark_root_block() { self::$root_block = null; } /** - * Check if block is a root block. + * Checks if block is a root block. * * @param array $block The block to check. - * * @return bool True if block is a root block, false otherwise. */ public static function is_marked_as_root_block( $block ) { + // If self::$root_block is null, is impossible that any block has been marked as root. + if ( is_null( self::$root_block ) ) { + return false; + } + // Blocks whose blockName is null are specifically intended to convey - "this is a freeform HTML block." + if ( null !== $block['blockName'] ) { + return str_contains( self::$root_block, $block['blockName'] ) && $block['blockName'] . md5( serialize( $block ) ) === self::$root_block; + } return md5( serialize( $block ) ) === self::$root_block; } /** - * Check if a root block has already been defined. + * Checks if a root block has already been defined. * - * @return bool True if block is a root block, false otherwise. + * @return bool True if there is a root block, false otherwise. */ public static function has_root_block() { return isset( self::$root_block ); } + /** + * Stores a reference to a direct children of an interactive block to be able + * to identify it later. + * + * @param array $block The block to add. + */ + public static function mark_children_of_interactive_block( $block ) { + self::$children_of_interactive_block[] = md5( serialize( $block ) ); + } /** - * Find the matching closing tag for an opening tag. + * Checks if block is marked as children of an interactive block. + * + * @param array $block The block to check. + * @return bool True if block is a children of an interactive block, false otherwise. + */ + public static function is_marked_as_children_of_interactive_block( $block ) { + return in_array( md5( serialize( $block ) ), self::$children_of_interactive_block, true ); + } + + /** + * Finds the matching closing tag for an opening tag. * * When called while on an open tag, traverse the HTML until we find the * matching closing tag, respecting any in-between content, including nested @@ -111,76 +137,7 @@ public function next_balanced_closer() { } /** - * 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. + * Returns the content between two balanced tags. * * When called on an opening tag, return the HTML content found between that * opening tag and its matching closing tag. @@ -206,14 +163,13 @@ public function get_inner_html() { } /** - * Set the content between two balanced tags. + * Sets the content between two balanced tags. * * When called on an opening tag, set the HTML content found between that * opening tag and its matching closing tag. * * @param string $new_html The string to replace the content between the * matching tags with. - * * @return bool Whether the content was successfully replaced. */ public function set_inner_html( $new_html ) { @@ -237,7 +193,7 @@ public function set_inner_html( $new_html ) { } /** - * Return a pair of bookmarks for the current opening tag and the matching + * Returns a pair of bookmarks for the current opening tag and the matching * closing tag. * * @return array|false A pair of bookmarks, or false if there's no matching @@ -267,12 +223,12 @@ public function get_balanced_tag_bookmarks() { } /** - * Whether a given HTML element is void (e.g.
). + * Checks whether a given HTML element is void (e.g.
). + * + * @see https://html.spec.whatwg.org/#elements-2 * * @param string $tag_name The element in question. * @return bool True if the element is void. - * - * @see https://html.spec.whatwg.org/#elements-2 */ public static function is_html_void_element( $tag_name ) { switch ( $tag_name ) { @@ -297,7 +253,7 @@ public static function is_html_void_element( $tag_name ) { } /** - * Extract and return the directive type and the the part after the double + * Extracts and return the directive type and the the part after the double * hyphen from an attribute name (if present), in an array format. * * Examples: @@ -307,9 +263,48 @@ public static function is_html_void_element( $tag_name ) { * 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' ) * * @param string $name The attribute name. - * @return array The resulting array + * @return array The resulting array. */ public static function parse_attribute_name( $name ) { return explode( '--', $name, 2 ); } + + /** + * Parse and extract the namespace and path from the given value. + * + * If the value contains a JSON instead of a path, the function parses it + * and returns the resulting array. + * + * @param string $value Passed value. + * @param string $ns Namespace fallback. + * @return array The resulting array + */ + public static function parse_attribute_value( $value, $ns = null ) { + $matches = array(); + $has_ns = preg_match( '/^([\w\-_\/]+)::(.+)$/', $value, $matches ); + + /* + * Overwrite both `$ns` and `$value` variables if `$value` explicitly + * contains a namespace. + */ + if ( $has_ns ) { + list( , $ns, $value ) = $matches; + } + + /* + * Try to decode `$value` as a JSON object. If it works, `$value` is + * replaced with the resulting array. The original string is preserved + * otherwise. + * + * Note that `json_decode` returns `null` both for an invalid JSON or + * the `'null'` string (a valid JSON). In the latter case, `$value` is + * replaced with `null`. + */ + $data = json_decode( $value, true ); + if ( null !== $data || 'null' === trim( $value ) ) { + $value = $data; + } + + return array( $ns, $value ); + } } diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php new file mode 100644 index 00000000000000..15e57edfa4a6a2 --- /dev/null +++ b/lib/experimental/interactivity-api/class-wp-interactivity-initial-state.php @@ -0,0 +1,82 @@ +%s', + wp_json_encode( self::$initial_state, JSON_HEX_TAG | JSON_HEX_AMP ) + ); + } +} diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-store.php b/lib/experimental/interactivity-api/class-wp-interactivity-store.php deleted file mode 100644 index c53701b14e8aff..00000000000000 --- a/lib/experimental/interactivity-api/class-wp-interactivity-store.php +++ /dev/null @@ -1,69 +0,0 @@ -%s', - wp_json_encode( self::$store, JSON_HEX_TAG | JSON_HEX_AMP ) - ); - } -} diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 064fc8ea62cbb2..b49ee538390ff1 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -1,16 +1,16 @@ '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(); + // Parse our own block delimiters for interactive and non-interactive blocks. + $parsed_blocks = parse_blocks( $block_content ); + $context = new WP_Directive_Context(); + $processed_content = ''; + $namespace_stack = array(); + foreach ( $parsed_blocks as $parsed_block ) { + if ( 'core/interactivity-wrapper' === $parsed_block['blockName'] ) { + $processed_content .= gutenberg_process_interactive_block( $parsed_block, $context, $namespace_stack ); + } elseif ( 'core/non-interactivity-wrapper' === $parsed_block['blockName'] ) { + $processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context, $namespace_stack ); + } else { + $processed_content .= $parsed_block['innerHTML']; + } + } + return $processed_content; } return $block_content; } -add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 ); +add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 20, 2 ); + +/** + * Marks the block as a children of an interactive block. + * + * @param array $parsed_block The parsed block. + * @param array $source_block The source block. + * @param WP_Block $parent_block The parent block. + */ +function gutenberg_mark_chidren_of_interactive_block( $parsed_block, $source_block, $parent_block ) { + if ( + isset( $parent_block ) && + isset( $parent_block->block_type->supports['interactivity'] ) && + $parent_block->block_type->supports['interactivity'] + ) { + WP_Directive_Processor::mark_children_of_interactive_block( $source_block ); + } + return $parsed_block; +} +add_filter( 'render_block_data', 'gutenberg_mark_chidren_of_interactive_block', 100, 3 ); + +/** + * Adds a comment delimiter to mark if the block is interactive or not. + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * @param WP_Block $block_instance The block instance. + */ +function gutenberg_mark_block_interactivity( $block_content, $block, $block_instance ) { + if ( + isset( $block_instance->block_type->supports['interactivity'] ) && + $block_instance->block_type->supports['interactivity'] + ) { + // Wraps the interactive block with a comment delimiter to be able to + // process it later. + return get_comment_delimited_block_content( + 'core/interactivity-wrapper', + array(), + $block_content + ); + } elseif ( WP_Directive_Processor::is_marked_as_children_of_interactive_block( $block ) ) { + // Wraps the non-interactive block with a comment delimiter to be able to + // skip it later. + return get_comment_delimited_block_content( + 'core/non-interactivity-wrapper', + array(), + $block_content + ); + } + return $block_content; +} +add_filter( 'render_block', 'gutenberg_mark_block_interactivity', 10, 3 ); + +/** + * Traverses the HTML of an interactive block, searching for Interactivity API + * directives and processing them. For the inner blocks, it calls the + * corresponding function depending on the wrapper type. + * + * @param array $interactive_block The interactive block to process. + * @param WP_Directive_Context $context The context to use when processing. + * @param array $namespace_stack Stack of namespackes passed by reference. + * + * @return string The processed HTML. + */ +function gutenberg_process_interactive_block( $interactive_block, $context, &$namespace_stack ) { + $block_index = 0; + $content = ''; + $interactive_inner_blocks = array(); + + foreach ( $interactive_block['innerContent'] as $inner_content ) { + if ( is_string( $inner_content ) ) { + $content .= $inner_content; + } else { + // This is an inner block. It may be an interactive block or a + // non-interactive block. + $content .= ''; + $interactive_inner_blocks[] = $interactive_block['innerBlocks'][ $block_index++ ]; + } + } + + return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks, $namespace_stack ); +} + +/** + * Returns the HTML of a non-interactive block without processing the + * directives. For the inner blocks, it calls the corresponding function + * depending on the wrapper type. + * + * @param array $non_interactive_block The non-interactive block to process. + * @param WP_Directive_Context $context The context to use when processing. + * @param array $namespace_stack Stack of namespackes passed by reference. + * + * @return string The processed HTML. + */ +function gutenberg_process_non_interactive_block( $non_interactive_block, $context, &$namespace_stack ) { + $block_index = 0; + $content = ''; + foreach ( $non_interactive_block['innerContent'] as $inner_content ) { + if ( is_string( $inner_content ) ) { + // This content belongs to a non interactive block and therefore it cannot + // contain directives. We add the HTML directly to the final output. + $content .= $inner_content; + } else { + // This is an inner block. It may be an interactive block or a + // non-interactive block. + $inner_block = $non_interactive_block['innerBlocks'][ $block_index++ ]; + + if ( 'core/interactivity-wrapper' === $inner_block['blockName'] ) { + $content .= gutenberg_process_interactive_block( $inner_block, $context, $namespace_stack ); + } elseif ( 'core/non-interactivity-wrapper' === $inner_block['blockName'] ) { + $content .= gutenberg_process_non_interactive_block( $inner_block, $context, $namespace_stack ); + } + } + } + return $content; +} +/** + * Processes interactive HTML by applying directives to the HTML tags. + * + * It uses the WP_Directive_Processor class to parse the HTML and apply the + * directives. If a tag contains a 'WP-INNER-BLOCKS' string and there are inner + * blocks to process, the function processes these inner blocks and replaces the + * 'WP-INNER-BLOCKS' tag in the HTML with those blocks. + * + * @param string $html The HTML to process. + * @param mixed $context The context to use when processing. + * @param array $inner_blocks The inner blocks to process. + * @param array $namespace_stack Stack of namespackes passed by reference. + * + * @return string The processed HTML. + */ +function gutenberg_process_interactive_html( $html, $context, $inner_blocks = array(), &$namespace_stack = array() ) { + static $directives = array( + 'data-wp-interactive' => 'gutenberg_interactivity_process_wp_interactive', + 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', + 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', + '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( $html ); + $prefix = 'data-wp-'; + $tag_stack = array(); + $inner_processed_blocks = array(); + $inner_blocks_index = 0; + while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + $tag_name = $tags->get_tag(); + + // Processes the inner blocks. + if ( str_contains( $tag_name, 'WP-INNER-BLOCKS' ) && ! empty( $inner_blocks ) && ! $tags->is_tag_closer() ) { + if ( 'core/interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) { + $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack ); + } elseif ( 'core/non-interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) { + $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context, $namespace_stack ); + } + } + 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 ) = $tags::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 ( + ! $tags::is_html_void_element( $tag_name ) && + ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) + ) { + $tag_stack[] = array( $tag_name, $attributes ); + } + } + + // Extract all directive names. They'll be used later on. + $directive_names = array_keys( $directives ); + $directive_names_rev = array_reverse( $directive_names ); + + /* + * Sort attributes by the order they appear in the `$directives` + * argument, considering it as the priority order in which + * directives should be processed. Note that the order is reversed + * for tag closers. + */ + $sorted_attrs = array_intersect( + $tags->is_tag_closer() + ? $directive_names_rev + : $directive_names, + $attributes + ); + + foreach ( $sorted_attrs as $attribute ) { + call_user_func_array( + $directives[ $attribute ], + array( + $tags, + $context, + end( $namespace_stack ), + &$namespace_stack, + ) + ); + } + } + + $processed_html = $tags->get_updated_html(); + + // Replaces the inner block tags with the content of each inner block + // processed. + if ( ! empty( $inner_processed_blocks ) ) { + foreach ( $inner_processed_blocks as $inner_block_tag => $inner_block_content ) { + if ( str_contains( $processed_html, $inner_block_tag ) ) { + $processed_html = str_replace( '<' . $inner_block_tag . '>', $inner_block_content, $processed_html ); + } + } + } + + return $processed_html; +} /** - * Resolve the reference using the store and the context from the provided path. + * Resolves the passed reference from the store and the context under the given + * namespace. + * + * A reference could be either a single path or a namespace followed by a path, + * separated by two colons, i.e, `namespace::path.to.prop`. If the reference + * contains a namespace, that namespace overrides the one passed as argument. * - * @param string $path Path. + * @param string $reference Reference value. + * @param string $ns Inherited namespace. * @param array $context Context data. - * @return mixed + * @return mixed Resolved value. */ -function gutenberg_interactivity_evaluate_reference( $path, array $context = array() ) { - $store = array_merge( - WP_Interactivity_Store::get_data(), - array( 'context' => $context ) +function gutenberg_interactivity_evaluate_reference( $reference, $ns, array $context = array() ) { + // Extract the namespace from the reference (if present). + list( $ns, $path ) = WP_Directive_Processor::parse_attribute_value( $reference, $ns ); + + $store = array( + 'state' => WP_Interactivity_Initial_State::get_state( $ns ), + 'context' => $context[ $ns ] ?? array(), ); /* - * Check first if the directive path is preceded by a negator operator (!), + * Checks first if the directive path is preceded by a negator operator (!), * indicating that the value obtained from the Interactivity Store (or the * passed context) using the subsequent path should be negated. */ $should_negate_value = '!' === $path[0]; - - $path = $should_negate_value ? substr( $path, 1 ) : $path; - $path_segments = explode( '.', $path ); - $current = $store; + $path = $should_negate_value ? substr( $path, 1 ) : $path; + $path_segments = explode( '.', $path ); + $current = $store; foreach ( $path_segments as $p ) { if ( isset( $current[ $p ] ) ) { $current = $current[ $p ]; @@ -89,7 +342,7 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr } /* - * Check if $current is an anonymous function or an arrow function, and if + * Checks if $current is an anonymous function or an arrow function, and if * so, call it passing the store. Other types of callables are ignored on * purpose, as arbitrary strings or arrays could be wrongly evaluated as * "callables". @@ -97,9 +350,14 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr * E.g., "file" is an string and a "callable" (the "file" function exists). */ if ( $current instanceof Closure ) { - $current = call_user_func( $current, $store ); + /* + * TODO: Figure out a way to implement derived state without having to + * pass the store as argument: + * + * $current = call_user_func( $current ); + */ } - // Return the opposite if it has a negator operator (!). + // Returns the opposite if it has a negator operator (!). return $should_negate_value ? ! $current : $current; } diff --git a/lib/experimental/interactivity-api/directives/wp-bind.php b/lib/experimental/interactivity-api/directives/wp-bind.php index 54be4a9faeb7d2..57d2e5deb23ab4 100644 --- a/lib/experimental/interactivity-api/directives/wp-bind.php +++ b/lib/experimental/interactivity-api/directives/wp-bind.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_bind( $tags, $context ) { +function gutenberg_interactivity_process_wp_bind( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_bind( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $reference = $tags->get_attribute( $attr ); + $value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); $tags->set_attribute( $bound_attr, $value ); } } diff --git a/lib/experimental/interactivity-api/directives/wp-class.php b/lib/experimental/interactivity-api/directives/wp-class.php index 741cc75b42c60e..ef91835be86fc1 100644 --- a/lib/experimental/interactivity-api/directives/wp-class.php +++ b/lib/experimental/interactivity-api/directives/wp-class.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_class( $tags, $context ) { +function gutenberg_interactivity_process_wp_class( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,8 +26,8 @@ function gutenberg_interactivity_process_wp_class( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $add_class = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $reference = $tags->get_attribute( $attr ); + $add_class = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); if ( $add_class ) { $tags->add_class( $class_name ); } else { diff --git a/lib/experimental/interactivity-api/directives/wp-context.php b/lib/experimental/interactivity-api/directives/wp-context.php index 7d92b0ac7b0c67..b41b47c86c78c3 100644 --- a/lib/experimental/interactivity-api/directives/wp-context.php +++ b/lib/experimental/interactivity-api/directives/wp-context.php @@ -10,19 +10,21 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_context( $tags, $context ) { +function gutenberg_interactivity_process_wp_context( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { $context->rewind_context(); return; } - $value = $tags->get_attribute( 'data-wp-context' ); + $attr_value = $tags->get_attribute( 'data-wp-context' ); - $new_context = json_decode( - is_string( $value ) && ! empty( $value ) ? $value : '{}', - true - ); + //Separate namespace and value from the context directive attribute. + list( $ns, $data ) = is_string( $attr_value ) && ! empty( $attr_value ) + ? WP_Directive_Processor::parse_attribute_value( $attr_value, $ns ) + : array( $ns, null ); - $context->set_context( $new_context ?? array() ); + // Add parsed data to the context under the corresponding namespace. + $context->set_context( array( $ns => is_array( $data ) ? $data : array() ) ); } diff --git a/lib/experimental/interactivity-api/directives/wp-interactive.php b/lib/experimental/interactivity-api/directives/wp-interactive.php new file mode 100644 index 00000000000000..9f3471a8b4e6a9 --- /dev/null +++ b/lib/experimental/interactivity-api/directives/wp-interactive.php @@ -0,0 +1,44 @@ +is_tag_closer() ) { + array_pop( $ns_stack ); + return; + } + + /* + * Decode the data-wp-interactive attribute. In the case it is not a valid + * JSON string, NULL is stored in `$island_data`. + */ + $island = $tags->get_attribute( 'data-wp-interactive' ); + $island_data = is_string( $island ) && ! empty( $island ) + ? json_decode( $island, true ) + : null; + + /* + * Push the newly defined namespace, or the current one if the island + * definition was invalid or does not contain a namespace. + * + * This is done because the function pops out the current namespace from the + * stack whenever it finds an island's closing tag, independently of whether + * the island definition was correct or it contained a valid namespace. + */ + $ns_stack[] = isset( $island_data ) && $island_data['namespace'] + ? $island_data['namespace'] + : $ns; +} diff --git a/lib/experimental/interactivity-api/directives/wp-style.php b/lib/experimental/interactivity-api/directives/wp-style.php index 9c37f9082c2c0b..16432e57282606 100644 --- a/lib/experimental/interactivity-api/directives/wp-style.php +++ b/lib/experimental/interactivity-api/directives/wp-style.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_style( $tags, $context ) { +function gutenberg_interactivity_process_wp_style( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -25,10 +26,10 @@ function gutenberg_interactivity_process_wp_style( $tags, $context ) { continue; } - $expr = $tags->get_attribute( $attr ); - $style_value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); + $reference = $tags->get_attribute( $attr ); + $style_value = gutenberg_interactivity_evaluate_reference( $reference, $ns, $context->get_context() ); if ( $style_value ) { - $style_attr = $tags->get_attribute( 'style' ); + $style_attr = $tags->get_attribute( 'style' ) ?? ''; $style_attr = gutenberg_interactivity_set_style( $style_attr, $style_name, $style_value ); $tags->set_attribute( 'style', $style_attr ); } else { diff --git a/lib/experimental/interactivity-api/directives/wp-text.php b/lib/experimental/interactivity-api/directives/wp-text.php index b0cfc98a74e702..c4c5bb27a31e10 100644 --- a/lib/experimental/interactivity-api/directives/wp-text.php +++ b/lib/experimental/interactivity-api/directives/wp-text.php @@ -11,8 +11,9 @@ * * @param WP_Directive_Processor $tags Tags. * @param WP_Directive_Context $context Directive context. + * @param string $ns Namespace. */ -function gutenberg_interactivity_process_wp_text( $tags, $context ) { +function gutenberg_interactivity_process_wp_text( $tags, $context, $ns ) { if ( $tags->is_tag_closer() ) { return; } @@ -22,6 +23,6 @@ function gutenberg_interactivity_process_wp_text( $tags, $context ) { return; } - $text = gutenberg_interactivity_evaluate_reference( $value, $context->get_context() ); + $text = gutenberg_interactivity_evaluate_reference( $value, $ns, $context->get_context() ); $tags->set_inner_html( esc_html( $text ) ); } diff --git a/lib/experimental/interactivity-api/initial-state.php b/lib/experimental/interactivity-api/initial-state.php new file mode 100644 index 00000000000000..a38d0da631f3c4 --- /dev/null +++ b/lib/experimental/interactivity-api/initial-state.php @@ -0,0 +1,29 @@ + gutenberg_url( '/build/modules/importmap-polyfill.min.js' ), - 'defer' => true, - ) - ); - } + $test = 'HTMLScriptElement.supports && HTMLScriptElement.supports("importmap")'; + $src = gutenberg_url( '/build/modules/importmap-polyfill.min.js' ); + + echo ( + // Test presence of feature... + '' + ); } /** @@ -273,4 +279,192 @@ function gutenberg_dequeue_module( $module_identifier ) { add_action( $modules_position, array( 'Gutenberg_Modules', 'print_module_preloads' ) ); // Prints the script that loads the import map polyfill in the footer. -add_action( 'wp_footer', array( 'Gutenberg_Modules', 'print_import_map_polyfill' ), 11 ); +add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_import_map_polyfill' ), 11 ); + +/** + * Add module fields from block metadata to WP_Block_Type settings. + * + * This filter allows us to register modules from block metadata and attach additional fields to + * WP_Block_Type instances. + * + * @param array $settings Array of determined settings for registering a block type. + * @param array $metadata Metadata provided for registering a block type. + */ +function gutenberg_filter_block_type_metadata_settings_register_modules( $settings, $metadata = null ) { + $module_fields = array( + 'viewModule' => 'view_module_ids', + ); + foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { + if ( ! empty( $settings[ $metadata_field_name ] ) ) { + $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; + } + if ( ! empty( $metadata[ $metadata_field_name ] ) ) { + $modules = $metadata[ $metadata_field_name ]; + $processed_modules = array(); + if ( is_array( $modules ) ) { + for ( $index = 0; $index < count( $modules ); $index++ ) { + $processed_modules[] = gutenberg_register_block_module_id( + $metadata, + $metadata_field_name, + $index + ); + } + } else { + $processed_modules[] = gutenberg_register_block_module_id( + $metadata, + $metadata_field_name + ); + } + $settings[ $settings_field_name ] = $processed_modules; + } + } + + return $settings; +} + +add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_modules', 10, 2 ); + +/** + * Enqueue modules associated with the block. + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * @param WP_Block $instance The block instance. + */ +function gutenberg_filter_render_block_enqueue_view_modules( $block_content, $parsed_block, $block_instance ) { + $block_type = $block_instance->block_type; + + if ( ! empty( $block_type->view_module_ids ) ) { + foreach ( $block_type->view_module_ids as $module_id ) { + gutenberg_enqueue_module( $module_id ); + } + } + + return $block_content; +} + +add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_modules', 10, 3 ); + +/** + * Finds a module ID for the selected block metadata field. It detects + * when a path to file was provided and finds a corresponding asset file + * with details necessary to register the module under an automatically + * generated module ID. + * + * This is analogous to the `register_block_script_handle` in WordPress Core. + * + * @param array $metadata Block metadata. + * @param string $field_name Field name to pick from metadata. + * @param int $index Optional. Index of the script to register when multiple items passed. + * Default 0. + * @return string Module ID. + */ +function gutenberg_register_block_module_id( $metadata, $field_name, $index = 0 ) { + if ( empty( $metadata[ $field_name ] ) ) { + return false; + } + + $module_id = $metadata[ $field_name ]; + if ( is_array( $module_id ) ) { + if ( empty( $module_id[ $index ] ) ) { + return false; + } + $module_id = $module_id[ $index ]; + } + + $module_path = remove_block_asset_path_prefix( $module_id ); + if ( $module_id === $module_path ) { + return $module_id; + } + + $path = dirname( $metadata['file'] ); + $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); + $module_id = gutenberg_generate_block_asset_module_id( $metadata['name'], $field_name, $index ); + $module_asset_path = wp_normalize_path( realpath( $module_asset_raw_path ) ); + + if ( empty( $module_asset_path ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + // This string is from WordPress Core. See `register_block_script_handle`. + // Translators: This is a translation from WordPress Core (default). No need to translate. + __( 'The asset file (%1$s) for the "%2$s" defined in "%3$s" block definition is missing.', 'default' ), + $module_asset_raw_path, + $field_name, + $metadata['name'] + ), + '6.5.0' + ); + return false; + } + + $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); + $module_uri = get_block_asset_url( $module_path_norm ); + $module_asset = require $module_asset_path; + $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array(); + + gutenberg_register_module( + $module_id, + $module_uri, + $module_dependencies, + isset( $module_asset['version'] ) ? $module_asset['version'] : false + ); + + return $module_id; +} + +/** + * Generates the module ID for an asset based on the name of the block + * and the field name provided. + * + * This is analogous to the `generate_block_asset_handle` in WordPress Core. + * + * @param string $block_name Name of the block. + * @param string $field_name Name of the metadata field. + * @param int $index Optional. Index of the asset when multiple items passed. + * Default 0. + * @return string Generated module ID for the block's field. + */ +function gutenberg_generate_block_asset_module_id( $block_name, $field_name, $index = 0 ) { + if ( str_starts_with( $block_name, 'core/' ) ) { + $asset_handle = str_replace( 'core/', 'wp-block-', $block_name ); + if ( str_starts_with( $field_name, 'editor' ) ) { + $asset_handle .= '-editor'; + } + if ( str_starts_with( $field_name, 'view' ) ) { + $asset_handle .= '-view'; + } + if ( $index > 0 ) { + $asset_handle .= '-' . ( $index + 1 ); + } + return $asset_handle; + } + + $field_mappings = array( + 'viewModule' => 'view-module', + ); + $asset_handle = str_replace( '/', '-', $block_name ) . + '-' . $field_mappings[ $field_name ]; + if ( $index > 0 ) { + $asset_handle .= '-' . ( $index + 1 ); + } + return $asset_handle; +} + +function gutenberg_register_view_module_ids_rest_field() { + register_rest_field( + 'block-type', + 'view_module_ids', + array( + 'get_callback' => function ( $item ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] ); + if ( isset( $block_type->view_module_ids ) ) { + return $block_type->view_module_ids; + } + return array(); + }, + ) + ); +} + +add_action( 'rest_api_init', 'gutenberg_register_view_module_ids_rest_field' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index b77a69b692ff1f..2d2e76273d2d59 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -133,19 +133,19 @@ function gutenberg_initialize_experiments_settings() { 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test Connections', 'gutenberg' ), + 'label' => __( 'Test connecting block attribute values to a custom field value', 'gutenberg' ), 'id' => 'gutenberg-connections', ) ); add_settings_field( 'gutenberg-pattern-partial-syncing', - __( 'Synced patterns partial syncing', 'gutenberg' ), + __( 'Pattern overrides', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test partial syncing of patterns', 'gutenberg' ), + 'label' => __( 'Test overrides in synced patterns', 'gutenberg' ), 'id' => 'gutenberg-pattern-partial-syncing', ) ); diff --git a/lib/load.php b/lib/load.php index 7dd30982dbf065..d413334227ee73 100644 --- a/lib/load.php +++ b/lib/load.php @@ -117,8 +117,8 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/disable-tinymce.php'; } -require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-store.php'; -require __DIR__ . '/experimental/interactivity-api/store.php'; +require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-initial-state.php'; +require __DIR__ . '/experimental/interactivity-api/initial-state.php'; require __DIR__ . '/experimental/interactivity-api/modules.php'; require __DIR__ . '/experimental/interactivity-api/class-wp-directive-processor.php'; require __DIR__ . '/experimental/interactivity-api/class-wp-directive-context.php'; @@ -128,6 +128,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/interactivity-api/directives/wp-class.php'; require __DIR__ . '/experimental/interactivity-api/directives/wp-style.php'; require __DIR__ . '/experimental/interactivity-api/directives/wp-text.php'; +require __DIR__ . '/experimental/interactivity-api/directives/wp-interactive.php'; require __DIR__ . '/experimental/modules/class-gutenberg-modules.php'; @@ -221,7 +222,6 @@ function () { require __DIR__ . '/global-styles-and-settings.php'; require __DIR__ . '/class-wp-theme-json-data-gutenberg.php'; require __DIR__ . '/class-wp-theme-json-gutenberg.php'; -require __DIR__ . '/class-wp-theme-json-schema-gutenberg.php'; require __DIR__ . '/class-wp-theme-json-resolver-gutenberg.php'; require __DIR__ . '/class-wp-duotone-gutenberg.php'; require __DIR__ . '/blocks.php'; @@ -253,7 +253,6 @@ function () { require __DIR__ . '/block-supports/duotone.php'; require __DIR__ . '/block-supports/shadow.php'; require __DIR__ . '/block-supports/background.php'; -require __DIR__ . '/block-supports/behaviors.php'; require __DIR__ . '/block-supports/pattern.php'; // Data views. diff --git a/package-lock.json b/package-lock.json index 960ae127ea85a4..96a14ec8eeb50e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.4.0-rc.1", + "version": "17.5.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.4.0-rc.1", + "version": "17.5.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -86,7 +86,7 @@ "devDependencies": { "@actions/core": "1.9.1", "@actions/github": "5.0.0", - "@ariakit/test": "^0.3.5", + "@ariakit/test": "^0.3.7", "@babel/core": "7.16.0", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-syntax-jsx": "7.16.0", @@ -212,7 +212,7 @@ "node-fetch": "2.6.1", "node-watch": "0.7.0", "npm-run-all": "4.1.5", - "patch-package": "6.2.2", + "patch-package": "8.0.0", "postcss": "8.4.16", "postcss-loader": "6.2.1", "prettier": "npm:wp-prettier@3.0.3", @@ -1628,13 +1628,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ariakit/core": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.10.tgz", + "integrity": "sha512-AcN+GSoVXuUOzKx5d3xPL3YsEHevh4PIO6QIt/mg/nRX1XQ6cvxQEiAjO/BJQm+/MVl7/VbuGBoTFjr0tPU6NQ==" + }, + "node_modules/@ariakit/react": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.12.tgz", + "integrity": "sha512-HxKMZZhWSkwwS/Sh9OdWyuNKQ2tjDAIQIy2KVI7IRa8ZQ6ze/4g3YLUHbfCxO7oDupXHfXaeZ4hWx8lP7l1U/g==", + "dependencies": { + "@ariakit/react-core": "0.3.12" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@ariakit/react-core": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.12.tgz", + "integrity": "sha512-w6P1A7TYb1fKUe9QbwaoTOWofl13g7TEuXdV4JyefJCQL1e9HQdEw9UL67I8aXRo8/cFHH94/z0N37t8hw5Ogg==", + "dependencies": { + "@ariakit/core": "0.3.10", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@ariakit/test": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.5.tgz", - "integrity": "sha512-7UCQBnJZ88JptkEnAXT7iSgtxEZiFwqdkKtxLCXDssTOJNatbFsnq0Jow324y41jGfAE2n4Lf5qY2FsZUPf9XQ==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.7.tgz", + "integrity": "sha512-rOa9pJA0ZfPPSI4SkDX41CsBcvxs6BmxgzFEElZWZo/uBBqtnr8ZL4oe5HySeZKEAHRH86XDqfxFISkhV76m5g==", "dev": true, "dependencies": { - "@ariakit/core": "0.3.8", + "@ariakit/core": "0.3.10", "@testing-library/dom": "^8.0.0 || ^9.0.0" }, "peerDependencies": { @@ -1650,12 +1685,6 @@ } } }, - "node_modules/@ariakit/test/node_modules/@ariakit/core": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.8.tgz", - "integrity": "sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==", - "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", @@ -19711,6 +19740,15 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -21203,13 +21241,14 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -24631,6 +24670,20 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -28068,108 +28121,12 @@ } }, "node_modules/find-yarn-workspace-root": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz", - "integrity": "sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==", - "dev": true, - "dependencies": { - "fs-extra": "^4.0.3", - "micromatch": "^3.1.4" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/find-yarn-workspace-root/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", "dev": true, "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" + "micromatch": "^4.0.2" } }, "node_modules/flat": { @@ -28603,9 +28560,12 @@ "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -33323,12 +33283,36 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/json-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", + "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stable-stringify/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -33366,6 +33350,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -42337,85 +42330,78 @@ } }, "node_modules/patch-package": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.2.2.tgz", - "integrity": "sha512-YqScVYkVcClUY0v8fF0kWOjDYopzIM8e3bj/RU1DPeEF14+dCGm6UeOYm4jvCyxqIEQ5/eJzmbWfDWnUleFNMg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", "dev": true, "dependencies": { "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "find-yarn-workspace-root": "^1.2.1", - "fs-extra": "^7.0.1", - "is-ci": "^2.0.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", - "minimist": "^1.2.0", + "minimist": "^1.2.6", + "open": "^7.4.2", "rimraf": "^2.6.3", - "semver": "^5.6.0", + "semver": "^7.5.3", "slash": "^2.0.0", - "tmp": "^0.0.33" + "tmp": "^0.0.33", + "yaml": "^2.2.2" }, "bin": { "patch-package": "index.js" }, "engines": { + "node": ">=14", "npm": ">5" } }, - "node_modules/patch-package/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/patch-package/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/patch-package/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4.8" + "node": ">= 8" } }, "node_modules/patch-package/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=10" } }, "node_modules/patch-package/node_modules/glob": { @@ -42438,6 +42424,64 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/patch-package/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/patch-package/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -42450,13 +42494,25 @@ "rimraf": "bin.js" } }, - "node_modules/patch-package/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/patch-package/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "bin": { - "semver": "bin/semver" + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/patch-package/node_modules/slash": { @@ -42468,6 +42524,51 @@ "node": ">=6" } }, + "node_modules/patch-package/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/patch-package/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/patch-package/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -44099,97 +44200,6 @@ } } }, - "node_modules/puppeteer-testing-library": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/puppeteer-testing-library/-/puppeteer-testing-library-0.5.0.tgz", - "integrity": "sha512-x72CJwpbdJaGEjBEpUHqeNVdDu36Hoqt6yodnyL/msNN7IoioKS+3ZmwLw/DIiDZJujUAtq3qltbIehA+uGWHQ==", - "dev": true, - "dependencies": { - "jest-diff": "^26.6.2" - }, - "peerDependencies": { - "puppeteer": "*" - } - }, - "node_modules/puppeteer-testing-library/node_modules/@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/puppeteer-testing-library/node_modules/@types/yargs": { - "version": "15.0.15", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.15.tgz", - "integrity": "sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/puppeteer-testing-library/node_modules/diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/puppeteer-testing-library/node_modules/jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - }, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/puppeteer-testing-library/node_modules/jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true, - "engines": { - "node": ">= 10.14.2" - } - }, - "node_modules/puppeteer-testing-library/node_modules/pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, - "dependencies": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/puppeteer-testing-library/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==", - "dev": true - }, "node_modules/pure-rand": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", @@ -47306,6 +47316,21 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -54248,7 +54273,7 @@ "version": "25.14.0", "license": "GPL-2.0-or-later", "dependencies": { - "@ariakit/react": "^0.3.10", + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -54307,41 +54332,6 @@ "react-dom": "^18.0.0" } }, - "packages/components/node_modules/@ariakit/core": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.8.tgz", - "integrity": "sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==" - }, - "packages/components/node_modules/@ariakit/react": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.10.tgz", - "integrity": "sha512-XRY69IOm8Oy+HSPoaspcVLAhLo3ToLhhJKSLK1voTAZtSzu5kUeUf4nUPxTzYFsvirKORZgOLAeNwuo1gPr61g==", - "dependencies": { - "@ariakit/react-core": "0.3.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ariakit" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - } - }, - "packages/components/node_modules/@ariakit/react-core": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.10.tgz", - "integrity": "sha512-CzSffcNlOyS2xuy21UB6fgJXi5LriJ9JrTSJzcgJmE+P9/WfQlplJC3L75d8O2yKgaGPeFnQ0hhDA6ItsI98eQ==", - "dependencies": { - "@ariakit/core": "0.3.8", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - } - }, "packages/components/node_modules/@floating-ui/react-dom": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", @@ -54763,7 +54753,6 @@ "filenamify": "^4.2.0", "jest-message-util": "^29.6.2", "jest-snapshot": "^29.6.2", - "puppeteer-testing-library": "^0.5.0", "uuid": "^9.0.1" }, "engines": { @@ -55213,6 +55202,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/url": "file:../url" }, @@ -55707,7 +55697,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.109.3", + "version": "1.110.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -55720,7 +55710,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.109.3", + "version": "1.110.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -55731,7 +55721,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.109.3", + "version": "1.110.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -57396,22 +57386,37 @@ } } }, + "@ariakit/core": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.10.tgz", + "integrity": "sha512-AcN+GSoVXuUOzKx5d3xPL3YsEHevh4PIO6QIt/mg/nRX1XQ6cvxQEiAjO/BJQm+/MVl7/VbuGBoTFjr0tPU6NQ==" + }, + "@ariakit/react": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.12.tgz", + "integrity": "sha512-HxKMZZhWSkwwS/Sh9OdWyuNKQ2tjDAIQIy2KVI7IRa8ZQ6ze/4g3YLUHbfCxO7oDupXHfXaeZ4hWx8lP7l1U/g==", + "requires": { + "@ariakit/react-core": "0.3.12" + } + }, + "@ariakit/react-core": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.12.tgz", + "integrity": "sha512-w6P1A7TYb1fKUe9QbwaoTOWofl13g7TEuXdV4JyefJCQL1e9HQdEw9UL67I8aXRo8/cFHH94/z0N37t8hw5Ogg==", + "requires": { + "@ariakit/core": "0.3.10", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + } + }, "@ariakit/test": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.5.tgz", - "integrity": "sha512-7UCQBnJZ88JptkEnAXT7iSgtxEZiFwqdkKtxLCXDssTOJNatbFsnq0Jow324y41jGfAE2n4Lf5qY2FsZUPf9XQ==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.3.7.tgz", + "integrity": "sha512-rOa9pJA0ZfPPSI4SkDX41CsBcvxs6BmxgzFEElZWZo/uBBqtnr8ZL4oe5HySeZKEAHRH86XDqfxFISkhV76m5g==", "dev": true, "requires": { - "@ariakit/core": "0.3.8", + "@ariakit/core": "0.3.10", "@testing-library/dom": "^8.0.0 || ^9.0.0" - }, - "dependencies": { - "@ariakit/core": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.8.tgz", - "integrity": "sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==", - "dev": true - } } }, "@aw-web-design/x-default-browser": { @@ -69367,7 +69372,7 @@ "@wordpress/components": { "version": "file:packages/components", "requires": { - "@ariakit/react": "^0.3.10", + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -69419,29 +69424,6 @@ "valtio": "1.7.0" }, "dependencies": { - "@ariakit/core": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.3.8.tgz", - "integrity": "sha512-LlSCwbyyozMX4ZEobpYGcv1LFqOdBTdTYPZw3lAVgLcFSNivsazi3NkKM9qNWNIu00MS+xTa0+RuIcuWAjlB2Q==" - }, - "@ariakit/react": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.3.10.tgz", - "integrity": "sha512-XRY69IOm8Oy+HSPoaspcVLAhLo3ToLhhJKSLK1voTAZtSzu5kUeUf4nUPxTzYFsvirKORZgOLAeNwuo1gPr61g==", - "requires": { - "@ariakit/react-core": "0.3.10" - } - }, - "@ariakit/react-core": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.3.10.tgz", - "integrity": "sha512-CzSffcNlOyS2xuy21UB6fgJXi5LriJ9JrTSJzcgJmE+P9/WfQlplJC3L75d8O2yKgaGPeFnQ0hhDA6ItsI98eQ==", - "requires": { - "@ariakit/core": "0.3.8", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" - } - }, "@floating-ui/react-dom": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", @@ -69724,7 +69706,6 @@ "filenamify": "^4.2.0", "jest-message-util": "^29.6.2", "jest-snapshot": "^29.6.2", - "puppeteer-testing-library": "^0.5.0", "uuid": "^9.0.1" }, "dependencies": { @@ -70065,6 +70046,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/url": "file:../url" } @@ -72009,6 +71991,12 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -73173,13 +73161,14 @@ } }, "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" } }, "caller-callsite": { @@ -75778,6 +75767,17 @@ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -78441,99 +78441,12 @@ } }, "find-yarn-workspace-root": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz", - "integrity": "sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", "dev": true, "requires": { - "fs-extra": "^4.0.3", - "micromatch": "^3.1.4" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - } + "micromatch": "^4.0.2" } }, "flat": { @@ -78864,9 +78777,9 @@ } }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -82390,6 +82303,26 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-stable-stringify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.0.tgz", + "integrity": "sha512-zfA+5SuwYN2VWqN1/5HZaDzQKLJHaBVMZIIM+wuYjdptkaQsqzDdqjqf+lZZJUuJq1aanHiY8LhH8LmH+qBYJA==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -82427,6 +82360,12 @@ "graceful-fs": "^4.1.6" } }, + "jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true + }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -89400,67 +89339,59 @@ "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==" }, "patch-package": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.2.2.tgz", - "integrity": "sha512-YqScVYkVcClUY0v8fF0kWOjDYopzIM8e3bj/RU1DPeEF14+dCGm6UeOYm4jvCyxqIEQ5/eJzmbWfDWnUleFNMg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", "dev": true, "requires": { "@yarnpkg/lockfile": "^1.1.0", - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "find-yarn-workspace-root": "^1.2.1", - "fs-extra": "^7.0.1", - "is-ci": "^2.0.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", "klaw-sync": "^6.0.0", - "minimist": "^1.2.0", + "minimist": "^1.2.6", + "open": "^7.4.2", "rimraf": "^2.6.3", - "semver": "^5.6.0", + "semver": "^7.5.3", "slash": "^2.0.0", - "tmp": "^0.0.33" + "tmp": "^0.0.33", + "yaml": "^2.2.2" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" } }, "glob": { @@ -89477,6 +89408,47 @@ "path-is-absolute": "^1.0.0" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -89486,10 +89458,19 @@ "glob": "^7.1.3" } }, - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "slash": { @@ -89497,6 +89478,36 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true } } }, @@ -90720,81 +90731,6 @@ } } }, - "puppeteer-testing-library": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/puppeteer-testing-library/-/puppeteer-testing-library-0.5.0.tgz", - "integrity": "sha512-x72CJwpbdJaGEjBEpUHqeNVdDu36Hoqt6yodnyL/msNN7IoioKS+3ZmwLw/DIiDZJujUAtq3qltbIehA+uGWHQ==", - "dev": true, - "requires": { - "jest-diff": "^26.6.2" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "15.0.15", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.15.tgz", - "integrity": "sha512-IziEYMU9XoVj8hWg7k+UJrXALkGFjWJhn5QFEv9q4p+v40oZhSuC135M38st8XPjICL7Ey4TV64ferBGUoJhBg==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", - "dev": true - }, - "jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - } - }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, - "pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - } - }, - "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==", - "dev": true - } - } - }, "pure-rand": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", @@ -93165,6 +93101,18 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", diff --git a/package.json b/package.json index 44a7a4a2d5bef3..684f35d408d3c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.4.0-rc.1", + "version": "17.5.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -98,7 +98,7 @@ "devDependencies": { "@actions/core": "1.9.1", "@actions/github": "5.0.0", - "@ariakit/test": "^0.3.5", + "@ariakit/test": "^0.3.7", "@babel/core": "7.16.0", "@babel/plugin-proposal-export-namespace-from": "7.18.9", "@babel/plugin-syntax-jsx": "7.16.0", @@ -224,7 +224,7 @@ "node-fetch": "2.6.1", "node-watch": "0.7.0", "npm-run-all": "4.1.5", - "patch-package": "6.2.2", + "patch-package": "8.0.0", "postcss": "8.4.16", "postcss-loader": "6.2.1", "prettier": "npm:wp-prettier@3.0.3", diff --git a/packages/a11y/CHANGELOG.md b/packages/a11y/CHANGELOG.md index 2096b2d3897b0c..b78906a8b23e15 100644 --- a/packages/a11y/CHANGELOG.md +++ b/packages/a11y/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.49.0 (2023-12-27) - ## 3.48.0 (2023-12-13) ## 3.47.0 (2023-11-29) diff --git a/packages/a11y/package.json b/packages/a11y/package.json index 8ae9c45b7433d9..399b5bd451455c 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/a11y", - "version": "3.49.0-prerelease", + "version": "3.48.0", "description": "Accessibility (a11y) utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index 4e920146b3efb9..98c1d6e0f96219 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 2.49.0 (2023-12-27) - ## 2.48.0 (2023-12-13) ## 2.47.0 (2023-11-29) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index fbcf81fdf4f6fe..01ac7d7961f260 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "2.49.0-prerelease", + "version": "2.48.0", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index cd51837a59c9ef..cd9a3d8582dd93 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 6.46.0 (2023-12-27) - ## 6.45.0 (2023-12-13) ## 6.44.0 (2023-11-29) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index fcd88a0099df68..be6d4adb079ee9 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "6.46.0-prerelease", + "version": "6.45.0", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/autop/CHANGELOG.md b/packages/autop/CHANGELOG.md index 523280e8f90510..301f58946ecdd8 100644 --- a/packages/autop/CHANGELOG.md +++ b/packages/autop/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.49.0 (2023-12-27) - ## 3.48.0 (2023-12-13) ## 3.47.0 (2023-11-29) diff --git a/packages/autop/package.json b/packages/autop/package.json index 7df62adc204845..8acd56c43fc8c1 100644 --- a/packages/autop/package.json +++ b/packages/autop/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/autop", - "version": "3.49.0-prerelease", + "version": "3.48.0", "description": "WordPress's automatic paragraph functions `autop` and `removep`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md index 70792ef0d1c331..bab957e21b3aee 100644 --- a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md +++ b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.32.0 (2023-12-27) - ## 4.31.0 (2023-12-13) ## 4.30.0 (2023-11-29) diff --git a/packages/babel-plugin-import-jsx-pragma/package.json b/packages/babel-plugin-import-jsx-pragma/package.json index c0cfeed10daaaf..e9f2b1b19596fa 100644 --- a/packages/babel-plugin-import-jsx-pragma/package.json +++ b/packages/babel-plugin-import-jsx-pragma/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.32.0-prerelease", + "version": "4.31.0", "description": "Babel transform plugin for automatically injecting an import to be used as the pragma for the React JSX Transform plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-makepot/CHANGELOG.md b/packages/babel-plugin-makepot/CHANGELOG.md index 68ad15f0ecdf5c..af6099fa128a14 100644 --- a/packages/babel-plugin-makepot/CHANGELOG.md +++ b/packages/babel-plugin-makepot/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 5.33.0 (2023-12-27) - ## 5.32.0 (2023-12-13) ## 5.31.0 (2023-11-29) diff --git a/packages/babel-plugin-makepot/package.json b/packages/babel-plugin-makepot/package.json index 486ef101ada3b9..14d1364e72bf1c 100644 --- a/packages/babel-plugin-makepot/package.json +++ b/packages/babel-plugin-makepot/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-makepot", - "version": "5.33.0-prerelease", + "version": "5.32.0", "description": "WordPress Babel internationalization (i18n) plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index 80e0fd46eaf106..1b4af03b532b01 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 7.33.0 (2023-12-27) - ## 7.32.0 (2023-12-13) ## 7.31.0 (2023-11-29) diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index a3b4c864bb6fe8..2461fb083c3fac 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-preset-default", - "version": "7.33.0-prerelease", + "version": "7.32.0", "description": "Default Babel preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/base-styles/CHANGELOG.md b/packages/base-styles/CHANGELOG.md index ada856801698d8..3f1e818df2efdd 100644 --- a/packages/base-styles/CHANGELOG.md +++ b/packages/base-styles/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.40.0 (2023-12-27) - ## 4.39.0 (2023-12-13) ## 4.38.0 (2023-11-29) diff --git a/packages/base-styles/package.json b/packages/base-styles/package.json index 86928afa0a515f..bc5984014ee6ce 100644 --- a/packages/base-styles/package.json +++ b/packages/base-styles/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/base-styles", - "version": "4.40.0-prerelease", + "version": "4.39.0", "description": "Base SCSS utilities and variables for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blob/CHANGELOG.md b/packages/blob/CHANGELOG.md index 49707413b65f6e..7dd54454a04885 100644 --- a/packages/blob/CHANGELOG.md +++ b/packages/blob/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.49.0 (2023-12-27) - ## 3.48.0 (2023-12-13) ## 3.47.0 (2023-11-29) diff --git a/packages/blob/package.json b/packages/blob/package.json index 16fc425e198787..b1bc807dfd0b40 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blob", - "version": "3.49.0-prerelease", + "version": "3.48.0", "description": "Blob utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-directory/CHANGELOG.md b/packages/block-directory/CHANGELOG.md index 3f05b3a30c5d54..2a9b8c670db457 100644 --- a/packages/block-directory/CHANGELOG.md +++ b/packages/block-directory/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.26.0 (2023-12-27) - ## 4.25.0 (2023-12-13) ## 4.24.0 (2023-11-29) diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 0dcd2f36ccd52b..2962204ac36e60 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.26.0-prerelease", + "version": "4.25.0", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 50202d709c84d8..64763eb66f7a87 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 12.17.0 (2023-12-27) - ## 12.16.0 (2023-12-13) ## 12.15.0 (2023-11-29) diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 98aa8a709b2362..1f9be9e2608b92 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-editor", - "version": "12.17.0-prerelease", + "version": "12.16.0", "description": "Generic block editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/src/components/block-canvas/index.js b/packages/block-editor/src/components/block-canvas/index.js index 7d64897690721c..fea9689cea5e73 100644 --- a/packages/block-editor/src/components/block-canvas/index.js +++ b/packages/block-editor/src/components/block-canvas/index.js @@ -63,8 +63,6 @@ export function ExperimentalBlockCanvas( { ref={ resetTypingRef } contentRef={ contentRef } style={ { - width: '100%', - height: '100%', ...iframeProps?.style, } } name="editor-canvas" diff --git a/packages/block-editor/src/components/block-canvas/style.scss b/packages/block-editor/src/components/block-canvas/style.scss new file mode 100644 index 00000000000000..54ccd407d74a21 --- /dev/null +++ b/packages/block-editor/src/components/block-canvas/style.scss @@ -0,0 +1,6 @@ +iframe[name="editor-canvas"] { + width: 100%; + height: 100%; + background-color: $white; + display: block; +} diff --git a/packages/block-editor/src/components/block-draggable/index.native.js b/packages/block-editor/src/components/block-draggable/index.native.js index d153843cae6a40..6128bf0cb179a2 100644 --- a/packages/block-editor/src/components/block-draggable/index.native.js +++ b/packages/block-editor/src/components/block-draggable/index.native.js @@ -9,7 +9,6 @@ import { import Animated, { runOnJS, runOnUI, - useAnimatedRef, useAnimatedStyle, useSharedValue, withDelay, @@ -39,7 +38,6 @@ import RCTAztecView from '@wordpress/react-native-aztec'; import useScrollWhenDragging from './use-scroll-when-dragging'; import DraggableChip from './draggable-chip'; import { store as blockEditorStore } from '../../store'; -import { useBlockListContext } from '../block-list/block-list-context'; import DroppingInsertionPoint from './dropping-insertion-point'; import useBlockDropZone from '../use-block-drop-zone'; import styles from './style.scss'; @@ -74,13 +72,10 @@ const BlockDraggableWrapper = ( { children, isRTL } ) => { const { selectBlock, startDraggingBlocks, stopDraggingBlocks } = useDispatch( blockEditorStore ); - const { scrollRef } = useBlockListContext(); - const animatedScrollRef = useAnimatedRef(); const { left, right } = useSafeAreaInsets(); const { width } = useSafeAreaFrame(); const safeAreaOffset = left + right; const contentWidth = width - safeAreaOffset; - animatedScrollRef( scrollRef ); const scroll = { offsetY: useSharedValue( 0 ), diff --git a/packages/block-editor/src/components/block-draggable/test/helpers.native.js b/packages/block-editor/src/components/block-draggable/test/helpers.native.js index 1d8e4fbb2afb6d..eb0689c13cfa1c 100644 --- a/packages/block-editor/src/components/block-draggable/test/helpers.native.js +++ b/packages/block-editor/src/components/block-draggable/test/helpers.native.js @@ -3,11 +3,12 @@ */ import { act, + advanceAnimationByFrames, fireEvent, initializeEditor, + screen, waitForStoreResolvers, within, - advanceAnimationByFrames, } from 'test/helpers'; import { fireGestureHandler } from 'react-native-gesture-handler/jest-utils'; import { State } from 'react-native-gesture-handler'; @@ -52,15 +53,15 @@ const DEFAULT_TOUCH_EVENTS = [ export const initializeWithBlocksLayouts = async ( blocks ) => { const initialHtml = blocks.map( ( block ) => block.html ).join( '\n' ); - const screen = await initializeEditor( { initialHtml } ); - const { getAllByLabelText } = screen; + await initializeEditor( { initialHtml } ); const waitPromises = []; + const blockListItems = screen.getAllByTestId( 'block-list-item-cell' ); + // Check that rendered block list items match expected block count. + expect( blockListItems.length ).toBe( blocks.length ); + blocks.forEach( ( block, index ) => { - const a11yLabel = new RegExp( - `${ block.name } Block\\. Row ${ index + 1 }` - ); - const [ element ] = getAllByLabelText( a11yLabel ); + const element = blockListItems[ index ]; // "onLayout" event will populate the blocks layouts data. fireEvent( element, 'layout', { nativeEvent: { layout: block.layout }, diff --git a/packages/block-editor/src/components/block-draggable/use-scroll-when-dragging.native.js b/packages/block-editor/src/components/block-draggable/use-scroll-when-dragging.native.js index ef5c437206bbd1..75e6c04ffa33b2 100644 --- a/packages/block-editor/src/components/block-draggable/use-scroll-when-dragging.native.js +++ b/packages/block-editor/src/components/block-draggable/use-scroll-when-dragging.native.js @@ -34,7 +34,7 @@ const VELOCITY_MULTIPLIER = 5000; export default function useScrollWhenDragging() { const { scrollRef } = useBlockListContext(); const animatedScrollRef = useAnimatedRef(); - animatedScrollRef( scrollRef ); + animatedScrollRef( scrollRef?.scrollViewRef ); const { height: windowHeight } = useWindowDimensions(); diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 18cd72cde057dc..9cb3b89a7bc252 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -92,7 +92,9 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { blockType: _blockType, topLevelLockedBlock: __unstableGetContentLockingParent( _selectedBlockClientId ) || - ( getTemplateLock( _selectedBlockClientId ) === 'contentOnly' + ( getTemplateLock( _selectedBlockClientId ) === 'contentOnly' || + ( _selectedBlockName === 'core/block' && + window.__experimentalPatternPartialSyncing ) ? _selectedBlockClientId : undefined ), }; diff --git a/packages/block-editor/src/components/block-list/block-list-item-cell.native.js b/packages/block-editor/src/components/block-list/block-list-item-cell.native.js index 80aa2589eb64f5..e15a3e1063cad2 100644 --- a/packages/block-editor/src/components/block-list/block-list-item-cell.native.js +++ b/packages/block-editor/src/components/block-list/block-list-item-cell.native.js @@ -52,7 +52,11 @@ function BlockListItemCell( { children, item: clientId, onLayout } ) { [ clientId, rootClientId, updateBlocksLayouts, onLayout ] ); - return { children }; + return ( + + { children } + + ); } export default BlockListItemCell; diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index 027ed12a7483ae..f58251ee665832 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useCallback, useMemo, useState } from '@wordpress/element'; +import { useCallback, useMemo, useState, useRef } from '@wordpress/element'; import { GlobalStylesContext, getMergedGlobalStyles, @@ -40,6 +40,7 @@ import BlockInvalidWarning from './block-invalid-warning'; import BlockOutline from './block-outline'; import { store as blockEditorStore } from '../../store'; import { useLayout } from './layout'; +import useScrollUponInsertion from './use-scroll-upon-insertion'; import { useSettings } from '../use-settings'; const EMPTY_ARRAY = []; @@ -103,6 +104,18 @@ function BlockWrapper( { ]; const accessible = ! ( isSelected || isDescendentBlockSelected ); + const ref = useRef(); + const [ isLayoutCalculated, setIsLayoutCalculated ] = useState(); + useScrollUponInsertion( { + clientId, + isSelected, + isLayoutCalculated, + elementRef: ref, + } ); + const onLayout = useCallback( () => { + setIsLayoutCalculated( true ); + }, [] ); + return ( x; @@ -140,7 +139,7 @@ export default function BlockList( { insertBlock( newBlock, blockCount ); }; - const scrollViewRef = useRef( null ); + const scrollRef = useRef( null ); const shouldFlatListPreventAutomaticScroll = () => blockInsertionPointIsVisible; @@ -236,15 +235,11 @@ export default function BlockList( { onLayout={ onLayout } testID="block-list-wrapper" > - { - // eslint-disable-next-line no-undef - __DEV__ && - } { isRootList ? ( @@ -254,9 +249,7 @@ export default function BlockList( { ? { removeClippedSubviews: false } : {} ) } // Disable clipping on Android to fix focus losing. See https://github.com/wordpress-mobile/gutenberg-mobile/pull/741#issuecomment-472746541 accessibilityLabel="block-list" - innerRef={ ( ref ) => { - scrollViewRef.current = ref; - } } + ref={ scrollRef } extraScrollHeight={ extraScrollHeight } keyboardShouldPersistTaps="always" scrollViewStyle={ { flex: 1 } } diff --git a/packages/block-editor/src/components/block-list/use-in-between-inserter.js b/packages/block-editor/src/components/block-list/use-in-between-inserter.js index a60453716ff29c..bd323ed057d733 100644 --- a/packages/block-editor/src/components/block-list/use-in-between-inserter.js +++ b/packages/block-editor/src/components/block-list/use-in-between-inserter.js @@ -28,6 +28,7 @@ export function useInBetweenInserter() { getTemplateLock, __unstableIsWithinBlockOverlay, getBlockEditingMode, + getBlockName, } = useSelect( blockEditorStore ); const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); @@ -75,7 +76,9 @@ export function useInBetweenInserter() { if ( getTemplateLock( rootClientId ) || - getBlockEditingMode( rootClientId ) === 'disabled' + getBlockEditingMode( rootClientId ) === 'disabled' || + ( getBlockName( rootClientId ) === 'core/block' && + window.__experimentalPatternPartialSyncing ) ) { return; } diff --git a/packages/block-editor/src/components/block-list/use-scroll-upon-insertion.native.js b/packages/block-editor/src/components/block-list/use-scroll-upon-insertion.native.js new file mode 100644 index 00000000000000..d224c4c7776716 --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-scroll-upon-insertion.native.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { useEffect } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { useBlockListContext } from './block-list-context'; +import { store as blockEditorStore } from '../../store'; + +const useScrollUponInsertion = ( { + clientId, + isSelected, + isLayoutCalculated, + elementRef, +} ) => { + const { scrollRef } = useBlockListContext(); + const wasBlockJustInserted = useSelect( + ( select ) => + !! select( blockEditorStore ).wasBlockJustInserted( + clientId, + 'inserter_menu' + ), + [ clientId ] + ); + useEffect( () => { + const lastScrollTo = scrollRef?.lastScrollTo.current; + const alreadyScrolledTo = lastScrollTo?.clientId === clientId; + if ( + alreadyScrolledTo || + ! isSelected || + ! scrollRef || + ! wasBlockJustInserted || + ! isLayoutCalculated + ) { + return; + } + scrollRef.scrollToElement( elementRef ); + lastScrollTo.clientId = clientId; + }, [ + isSelected, + scrollRef, + wasBlockJustInserted, + elementRef, + isLayoutCalculated, + clientId, + ] ); +}; + +export default useScrollUponInsertion; diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js index 14a941a9011b6d..ccf04c5e5262d7 100644 --- a/packages/block-editor/src/components/block-lock/toolbar.js +++ b/packages/block-editor/src/components/block-lock/toolbar.js @@ -3,9 +3,8 @@ */ import { __ } from '@wordpress/i18n'; import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; -import { focus } from '@wordpress/dom'; import { useReducer, useRef, useEffect } from '@wordpress/element'; -import { lock } from '@wordpress/icons'; +import { lock, unlock } from '@wordpress/icons'; /** * Internal dependencies @@ -13,58 +12,28 @@ import { lock } from '@wordpress/icons'; import BlockLockModal from './modal'; import useBlockLock from './use-block-lock'; -export default function BlockLockToolbar( { clientId, wrapperRef } ) { - const { canEdit, canMove, canRemove, canLock } = useBlockLock( clientId ); +export default function BlockLockToolbar( { clientId } ) { + const { canLock, isLocked } = useBlockLock( clientId ); const [ isModalOpen, toggleModal ] = useReducer( ( isActive ) => ! isActive, false ); - const lockButtonRef = useRef( null ); - const isFirstRender = useRef( true ); - const hasModalOpened = useRef( false ); + const hasLockButtonShown = useRef( false ); - const shouldHideBlockLockUI = - ! canLock || ( canEdit && canMove && canRemove ); - - // Restore focus manually on the first focusable element in the toolbar - // when the block lock modal is closed and the block is not locked anymore. - // See https://github.com/WordPress/gutenberg/issues/51447 + // If the block lock button has been shown, we don't want to remove it + // from the toolbar until the toolbar is rendered again without it. + // Removing it beforehand can cause focus loss issues, such as when + // unlocking the block from the modal. It needs to return focus from + // whence it came, and to do that, we need to leave the button in the toolbar. useEffect( () => { - if ( isFirstRender.current ) { - isFirstRender.current = false; - return; - } - - if ( isModalOpen && ! hasModalOpened.current ) { - hasModalOpened.current = true; - } - - // We only want to allow this effect to happen if the modal has been opened. - // The issue is when we're returning focus from the block lock modal to a toolbar, - // so it can only happen after a modal has been opened. Without this, the toolbar - // will steal focus on rerenders. - if ( - hasModalOpened.current && - ! isModalOpen && - shouldHideBlockLockUI - ) { - focus.focusable - .find( wrapperRef.current, { - sequential: false, - } ) - .find( - ( element ) => - element.tagName === 'BUTTON' && - element !== lockButtonRef.current - ) - ?.focus(); + if ( isLocked ) { + hasLockButtonShown.current = true; } - // wrapperRef is a reference object and should be stable - }, [ isModalOpen, shouldHideBlockLockUI, wrapperRef ] ); + }, [ isLocked ] ); - if ( shouldHideBlockLockUI ) { + if ( ! canLock || ( ! isLocked && ! hasLockButtonShown.current ) ) { return null; } @@ -72,9 +41,8 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) { <> @@ -97,7 +98,7 @@ function BlockPattern( { { 'block-editor-block-patterns-list__list-item-synced': pattern.type === - PATTERN_TYPES.user && + INSERTER_PATTERN_TYPES.user && ! pattern.syncStatus, } ) } @@ -122,7 +123,8 @@ function BlockPattern( { /> - { pattern.type === PATTERN_TYPES.user && + { pattern.type === + INSERTER_PATTERN_TYPES.user && ! pattern.syncStatus && (
) } { ( ! showTooltip || - pattern.type === PATTERN_TYPES.user ) && ( + pattern.type === + INSERTER_PATTERN_TYPES.user ) && (
{ pattern.title }
diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 7bb52a7e8f0906..0d9b61314c4ed1 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -175,8 +175,7 @@ export function PrivateBlockToolbar( { { ! isMultiToolbar && ( ) } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js b/packages/block-editor/src/components/global-styles/get-global-styles-changes.js similarity index 54% rename from packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js rename to packages/block-editor/src/components/global-styles/get-global-styles-changes.js index fed075eb923ff4..942d709e6a268d 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js +++ b/packages/block-editor/src/components/global-styles/get-global-styles-changes.js @@ -1,32 +1,47 @@ +/** + * External dependencies + */ +import memoize from 'memize'; + /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { getBlockTypes } from '@wordpress/blocks'; const globalStylesChangesCache = new Map(); const EMPTY_ARRAY = []; - const translationMap = { caption: __( 'Caption' ), link: __( 'Link' ), button: __( 'Button' ), heading: __( 'Heading' ), + h1: __( 'H1' ), + h2: __( 'H2' ), + h3: __( 'H3' ), + h4: __( 'H4' ), + h5: __( 'H5' ), + h6: __( 'H6' ), 'settings.color': __( 'Color settings' ), 'settings.typography': __( 'Typography settings' ), 'styles.color': __( 'Colors' ), 'styles.spacing': __( 'Spacing' ), 'styles.typography': __( 'Typography' ), }; - +const getBlockNames = memoize( () => + getBlockTypes().reduce( ( accumulator, { name, title } ) => { + accumulator[ name ] = title; + return accumulator; + }, {} ) +); const isObject = ( obj ) => obj !== null && typeof obj === 'object'; /** * Get the translation for a given global styles key. - * @param {string} key A key representing a path to a global style property or setting. - * @param {Record} blockNames A key/value pair object of block names and their rendered titles. + * @param {string} key A key representing a path to a global style property or setting. * @return {string|undefined} A translated key or undefined if no translation exists. */ -function getTranslation( key, blockNames ) { +function getTranslation( key ) { if ( translationMap[ key ] ) { return translationMap[ key ]; } @@ -34,7 +49,7 @@ function getTranslation( key, blockNames ) { const keyArray = key.split( '.' ); if ( keyArray?.[ 0 ] === 'blocks' ) { - const blockName = blockNames[ keyArray[ 1 ] ]; + const blockName = getBlockNames()?.[ keyArray[ 1 ] ]; return blockName ? sprintf( // translators: %s: block name. @@ -45,10 +60,11 @@ function getTranslation( key, blockNames ) { } if ( keyArray?.[ 0 ] === 'elements' ) { + const elementName = translationMap[ keyArray[ 1 ] ] || keyArray[ 1 ]; return sprintf( // translators: %s: element name, e.g., heading button, link, caption. __( '%s element' ), - translationMap[ keyArray[ 1 ] ] + elementName ); } @@ -99,50 +115,45 @@ function deepCompare( changedObject, originalObject, parentPath = '' ) { } /** - * Get an array of translated summarized global styles changes. - * Results are cached using a Map() key of `JSON.stringify( { revision, previousRevision } )`. + * Returns an array of translated summarized global styles changes. + * Results are cached using a Map() key of `JSON.stringify( { next, previous } )`. * - * @param {Object} revision The changed object to compare. - * @param {Object} previousRevision The original object to compare against. - * @param {Record} blockNames A key/value pair object of block names and their rendered titles. - * @return {string[]} An array of translated changes. + * @param {Object} next The changed object to compare. + * @param {Object} previous The original object to compare against. + * @return {string[]} An array of translated changes. */ -export default function getRevisionChanges( - revision, - previousRevision, - blockNames -) { - const cacheKey = JSON.stringify( { revision, previousRevision } ); +function getGlobalStylesChangelist( next, previous ) { + const cacheKey = JSON.stringify( { next, previous } ); if ( globalStylesChangesCache.has( cacheKey ) ) { return globalStylesChangesCache.get( cacheKey ); } /* - * Compare the two revisions with normalized keys. + * Compare the two changesets with normalized keys. * The order of these keys determines the order in which * they'll appear in the results. */ const changedValueTree = deepCompare( { styles: { - color: revision?.styles?.color, - typography: revision?.styles?.typography, - spacing: revision?.styles?.spacing, + color: next?.styles?.color, + typography: next?.styles?.typography, + spacing: next?.styles?.spacing, }, - blocks: revision?.styles?.blocks, - elements: revision?.styles?.elements, - settings: revision?.settings, + blocks: next?.styles?.blocks, + elements: next?.styles?.elements, + settings: next?.settings, }, { styles: { - color: previousRevision?.styles?.color, - typography: previousRevision?.styles?.typography, - spacing: previousRevision?.styles?.spacing, + color: previous?.styles?.color, + typography: previous?.styles?.typography, + spacing: previous?.styles?.spacing, }, - blocks: previousRevision?.styles?.blocks, - elements: previousRevision?.styles?.elements, - settings: previousRevision?.settings, + blocks: previous?.styles?.blocks, + elements: previous?.styles?.elements, + settings: previous?.settings, } ); @@ -158,7 +169,7 @@ export default function getRevisionChanges( * Remove duplicate or empty translations. */ .reduce( ( acc, curr ) => { - const translation = getTranslation( curr, blockNames ); + const translation = getTranslation( curr ); if ( translation && ! acc.includes( translation ) ) { acc.push( translation ); } @@ -169,3 +180,31 @@ export default function getRevisionChanges( return result; } + +/** + * From a getGlobalStylesChangelist() result, returns a truncated array of translated changes. + * Appends a translated string indicating the number of changes that were truncated. + * + * @param {Object} next The changed object to compare. + * @param {Object} previous The original object to compare against. + * @param {{maxResults:number}} options Options. maxResults: results to return before truncating. + * @return {string[]} An array of translated changes. + */ +export default function getGlobalStylesChanges( next, previous, options = {} ) { + const changes = getGlobalStylesChangelist( next, previous ); + const changesLength = changes.length; + const { maxResults } = options; + + // Truncate to `n` results if necessary. + if ( !! maxResults && changesLength && changesLength > maxResults ) { + const deleteCount = changesLength - maxResults; + const andMoreText = sprintf( + // translators: %d: number of global styles changes that are not displayed in the UI. + _n( '…and %d more change.', '…and %d more changes.', deleteCount ), + deleteCount + ); + changes.splice( maxResults, deleteCount, andMoreText ); + } + + return changes; +} diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 76a95357ba52b4..65392a7636c442 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -29,3 +29,4 @@ export { } from './image-settings-panel'; export { default as AdvancedPanel } from './advanced-panel'; export { areGlobalStyleConfigsEqual } from './utils'; +export { default as getGlobalStylesChanges } from './get-global-styles-changes'; diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/test/get-revision-changes.js b/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js similarity index 67% rename from packages/edit-site/src/components/global-styles/screen-revisions/test/get-revision-changes.js rename to packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js index be9a26b97f6885..0c53336c87e02b 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/test/get-revision-changes.js +++ b/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js @@ -1,9 +1,33 @@ /** * Internal dependencies */ -import getRevisionChanges from '../get-revision-changes'; +import getGlobalStylesChanges from '../get-global-styles-changes'; + +/** + * WordPress dependencies + */ +import { + registerBlockType, + unregisterBlockType, + getBlockTypes, +} from '@wordpress/blocks'; + +describe( 'getGlobalStylesChanges', () => { + beforeEach( () => { + registerBlockType( 'core/test-fiori-di-zucca', { + save: () => {}, + category: 'text', + title: 'Test pumpkin flowers', + edit: () => {}, + } ); + } ); + + afterEach( () => { + getBlockTypes().forEach( ( block ) => { + unregisterBlockType( block.name ); + } ); + } ); -describe( 'getRevisionChanges', () => { const revision = { id: 10, styles: { @@ -29,6 +53,11 @@ describe( 'getRevisionChanges', () => { letterSpacing: '37px', }, }, + h3: { + typography: { + lineHeight: '1.2', + }, + }, caption: { color: { text: 'var(--wp--preset--color--pineapple)', @@ -39,7 +68,7 @@ describe( 'getRevisionChanges', () => { text: 'var(--wp--preset--color--tomato)', }, blocks: { - 'core/paragraph': { + 'core/test-fiori-di-zucca': { color: { text: '#000000', }, @@ -84,6 +113,16 @@ describe( 'getRevisionChanges', () => { letterSpacing: '37px', }, }, + h3: { + typography: { + lineHeight: '2', + }, + }, + h6: { + typography: { + lineHeight: '1.2', + }, + }, caption: { typography: { fontSize: '1.11rem', @@ -106,7 +145,7 @@ describe( 'getRevisionChanges', () => { background: 'var(--wp--preset--color--pumpkin)', }, blocks: { - 'core/paragraph': { + 'core/test-fiori-di-zucca': { color: { text: '#fff', }, @@ -126,35 +165,39 @@ describe( 'getRevisionChanges', () => { }, }, }; - const blockNames = { - 'core/paragraph': 'Paragraph', - }; + it( 'returns a list of changes and caches them', () => { - const resultA = getRevisionChanges( - revision, - previousRevision, - blockNames - ); + const resultA = getGlobalStylesChanges( revision, previousRevision ); expect( resultA ).toEqual( [ 'Colors', 'Typography', - 'Paragraph block', + 'Test pumpkin flowers block', + 'H3 element', 'Caption element', + 'H6 element', 'Link element', 'Color settings', ] ); - const resultB = getRevisionChanges( - revision, - previousRevision, - blockNames - ); + const resultB = getGlobalStylesChanges( revision, previousRevision ); expect( resultA ).toBe( resultB ); } ); + it( 'returns a list of truncated changes', () => { + const resultA = getGlobalStylesChanges( revision, previousRevision, { + maxResults: 3, + } ); + expect( resultA ).toEqual( [ + 'Colors', + 'Typography', + 'Test pumpkin flowers block', + '…and 5 more changes.', + ] ); + } ); + it( 'skips unknown and unchanged keys', () => { - const result = getRevisionChanges( + const result = getGlobalStylesChanges( { styles: { frogs: { diff --git a/packages/block-editor/src/components/height-control/README.md b/packages/block-editor/src/components/height-control/README.md index 8853f9ef89321e..9be1741e8cdd8e 100644 --- a/packages/block-editor/src/components/height-control/README.md +++ b/packages/block-editor/src/components/height-control/README.md @@ -2,7 +2,7 @@ The `HeightControl` component adds a linked unit control and slider component for controlling the height of a block within the block editor. It supports passing a label, and is used for controlling the minimum height dimensions of Group blocks. -_Note:_ It is worth noting that the minimum height option is an opt-in feature. Themes need to declare support for it before it'll be available, and a convenient way to do that is via opting in to the [appearanceTools](/docs/how-to-guides/themes/theme-json/#opt-in-into-ui-controls) UI controls. +_Note:_ It is worth noting that the minimum height option is an opt-in feature. Themes need to declare support for it before it'll be available, and a convenient way to do that is via opting in to the [appearanceTools](/docs/how-to-guides/themes/global-settings-and-styles.md#opt-in-into-ui-controls) UI controls. ## Development guidelines @@ -43,7 +43,7 @@ A callback function that handles the application of the height value. - **Type:** `String` - **Default:** `'Height'` -A label for the height control. This is useful when using the height control for a feature that is controlled in the same way as height, but requires a different label. For example, "Min. height". +A label for the height control. This is useful when using the height control for a feature that is controlled in the same way as height, but requires a different label. For example, "Minimum height". ## Related components diff --git a/packages/block-editor/src/components/height-control/index.js b/packages/block-editor/src/components/height-control/index.js index e1797522497447..23738378b69983 100644 --- a/packages/block-editor/src/components/height-control/index.js +++ b/packages/block-editor/src/components/height-control/index.js @@ -157,6 +157,8 @@ export default function HeightControl( { onUnitChange={ handleUnitChange } min={ 0 } size={ '__unstable-large' } + label={ label } + hideLabelFromVision /> @@ -175,6 +177,8 @@ export default function HeightControl( { withInputField={ false } onChange={ handleSliderChange } __nextHasNoMarginBottom + label={ label } + hideLabelFromVision /> diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 45451908a34472..21a9b1114ce5fa 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -45,13 +45,13 @@ export { MEDIA_TYPE_AUDIO, MEDIA_TYPE_ANY, } from './media-upload/constants'; +export { default as MediaUploadProgress } from './media-upload-progress'; export { - default as MediaUploadProgress, MEDIA_UPLOAD_STATE_UPLOADING, MEDIA_UPLOAD_STATE_SUCCEEDED, MEDIA_UPLOAD_STATE_FAILED, MEDIA_UPLOAD_STATE_RESET, -} from './media-upload-progress'; +} from './media-upload-progress/constants'; export { default as BlockMediaUpdateProgress } from './block-media-update-progress'; export { default as URLInput } from './url-input'; export { default as BlockInvalidWarning } from './block-list/block-invalid-warning'; diff --git a/packages/block-editor/src/components/inserter-draggable-blocks/index.js b/packages/block-editor/src/components/inserter-draggable-blocks/index.js index c6f008426288e1..cdaadcb0f36eb5 100644 --- a/packages/block-editor/src/components/inserter-draggable-blocks/index.js +++ b/packages/block-editor/src/components/inserter-draggable-blocks/index.js @@ -12,7 +12,7 @@ import { useSelect } from '@wordpress/data'; * Internal dependencies */ import BlockDraggableChip from '../block-draggable/draggable-chip'; -import { PATTERN_TYPES } from '../inserter/block-patterns-tab/utils'; +import { INSERTER_PATTERN_TYPES } from '../inserter/block-patterns-tab/utils'; const InserterDraggableBlocks = ( { isEnabled, @@ -42,7 +42,7 @@ const InserterDraggableBlocks = ( { transferData={ transferData } onDragStart={ ( event ) => { const parsedBlocks = - pattern?.type === PATTERN_TYPES.user && + pattern?.type === INSERTER_PATTERN_TYPES.user && pattern?.syncStatus !== 'unsynced' ? [ createBlock( 'core/block', { ref: pattern.id } ) ] : blocks; diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js index bf2867be5cdf3c..217f3f21b573df 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js @@ -18,7 +18,7 @@ import { searchItems } from '../search-items'; import BlockPatternsPaging from '../../block-patterns-paging'; import usePatternsPaging from '../hooks/use-patterns-paging'; import { - PATTERN_TYPES, + INSERTER_PATTERN_TYPES, allPatternsCategory, myPatternsCategory, } from '../block-patterns-tab/utils'; @@ -73,7 +73,7 @@ function PatternList( { searchValue, selectedCategory, patternCategories } ) { } if ( selectedCategory === myPatternsCategory.name && - pattern.type === PATTERN_TYPES.user + pattern.type === INSERTER_PATTERN_TYPES.user ) { return true; } 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 index 071a9c479003fa..7b8fd8e76202a2 100644 --- 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 @@ -30,7 +30,7 @@ import { isPatternFiltered, allPatternsCategory, myPatternsCategory, - PATTERN_TYPES, + INSERTER_PATTERN_TYPES, } from './utils'; const noop = () => {}; @@ -73,7 +73,7 @@ export function PatternCategoryPreviews( { if ( category.name === myPatternsCategory.name && - pattern.type === PATTERN_TYPES.user + pattern.type === INSERTER_PATTERN_TYPES.user ) { return true; } diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js b/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js index 04a0f27d162dd4..e6056c07f0d472 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/patterns-filter.js @@ -16,11 +16,13 @@ import { useMemo, createInterpolateElement } from '@wordpress/element'; /** * Internal dependencies */ -import { myPatternsCategory, SYNC_TYPES, PATTERN_TYPES } from './utils'; - -const getShouldDisableSyncFilter = ( sourceFilter ) => - sourceFilter !== PATTERN_TYPES.all && sourceFilter !== PATTERN_TYPES.user; +import { + myPatternsCategory, + INSERTER_SYNC_TYPES, + INSERTER_PATTERN_TYPES, +} from './utils'; +const getShouldDisableSyncFilter = ( sourceFilter ) => sourceFilter !== 'all'; const getShouldDisableNonUserSources = ( category ) => { return category.name === myPatternsCategory.name; }; @@ -39,7 +41,7 @@ export function PatternsFilter( { // this filter themselves. const currentPatternSourceFilter = category.name === myPatternsCategory.name - ? PATTERN_TYPES.user + ? INSERTER_PATTERN_TYPES.user : patternSourceFilter; // We need to disable the sync filter option if the source filter is not 'all' or 'user' @@ -56,11 +58,11 @@ export function PatternsFilter( { const patternSyncMenuOptions = useMemo( () => [ { - value: SYNC_TYPES.all, + value: 'all', label: _x( 'All', 'Option that shows all patterns' ), }, { - value: SYNC_TYPES.full, + value: INSERTER_SYNC_TYPES.full, label: _x( 'Synced', 'Option that shows all synchronized patterns' @@ -68,7 +70,7 @@ export function PatternsFilter( { disabled: shouldDisableSyncFilter, }, { - value: SYNC_TYPES.unsynced, + value: INSERTER_SYNC_TYPES.unsynced, label: _x( 'Not synced', 'Option that shows all patterns that are not synchronized' @@ -82,22 +84,22 @@ export function PatternsFilter( { const patternSourceMenuOptions = useMemo( () => [ { - value: PATTERN_TYPES.all, + value: 'all', label: __( 'All' ), disabled: shouldDisableNonUserSources, }, { - value: PATTERN_TYPES.directory, + value: INSERTER_PATTERN_TYPES.directory, label: __( 'Pattern Directory' ), disabled: shouldDisableNonUserSources, }, { - value: PATTERN_TYPES.theme, + value: INSERTER_PATTERN_TYPES.theme, label: __( 'Theme & Plugins' ), disabled: shouldDisableNonUserSources, }, { - value: PATTERN_TYPES.user, + value: INSERTER_PATTERN_TYPES.user, label: __( 'User' ), }, ], @@ -107,7 +109,7 @@ export function PatternsFilter( { function handleSetSourceFilterChange( newSourceFilter ) { setPatternSourceFilter( newSourceFilter ); if ( getShouldDisableSyncFilter( newSourceFilter ) ) { - setPatternSyncFilter( SYNC_TYPES.all ); + setPatternSyncFilter( 'all' ); } } 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 index 9f4d598ce37cbf..cff5fbf3413820 100644 --- 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 @@ -14,7 +14,7 @@ import { isPatternFiltered, allPatternsCategory, myPatternsCategory, - PATTERN_TYPES, + INSERTER_PATTERN_TYPES, } from './utils'; function hasRegisteredCategory( pattern, allCategories ) { @@ -69,7 +69,7 @@ export function usePatternCategories( rootClientId, sourceFilter = 'all' ) { } if ( filteredPatterns.some( - ( pattern ) => pattern.type === PATTERN_TYPES.user + ( pattern ) => pattern.type === INSERTER_PATTERN_TYPES.user ) ) { categories.unshift( myPatternsCategory ); 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 index b1e5a99bbe6dc6..189c87c39e5edc 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js @@ -4,17 +4,13 @@ import { __ } from '@wordpress/i18n'; -export const PATTERN_TYPES = { - all: 'all', - synced: 'synced', - unsynced: 'unsynced', +export const INSERTER_PATTERN_TYPES = { user: 'user', theme: 'theme', directory: 'directory', }; -export const SYNC_TYPES = { - all: 'all', +export const INSERTER_SYNC_TYPES = { full: 'fully', unsynced: 'unsynced', }; @@ -38,7 +34,7 @@ export function isPatternFiltered( pattern, sourceFilter, syncFilter ) { // If theme source selected, filter out user created patterns and those from // the core patterns directory. if ( - sourceFilter === PATTERN_TYPES.theme && + sourceFilter === INSERTER_PATTERN_TYPES.theme && ( isUserPattern || isDirectoryPattern ) ) { return true; @@ -47,7 +43,7 @@ export function isPatternFiltered( pattern, sourceFilter, syncFilter ) { // If the directory source is selected, filter out user created patterns // and those bundled with the theme. if ( - sourceFilter === PATTERN_TYPES.directory && + sourceFilter === INSERTER_PATTERN_TYPES.directory && ( isUserPattern || ! isDirectoryPattern ) ) { return true; @@ -55,19 +51,22 @@ export function isPatternFiltered( pattern, sourceFilter, syncFilter ) { // If user source selected, filter out theme patterns. if ( - sourceFilter === PATTERN_TYPES.user && - pattern.type !== PATTERN_TYPES.user + sourceFilter === INSERTER_PATTERN_TYPES.user && + pattern.type !== INSERTER_PATTERN_TYPES.user ) { return true; } // Filter by sync status. - if ( syncFilter === SYNC_TYPES.full && pattern.syncStatus !== '' ) { + if ( + syncFilter === INSERTER_SYNC_TYPES.full && + pattern.syncStatus !== '' + ) { return true; } if ( - syncFilter === SYNC_TYPES.unsynced && + syncFilter === INSERTER_SYNC_TYPES.unsynced && pattern.syncStatus !== 'unsynced' && isUserPattern ) { diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js index 576768c76abca9..b5c8c8551b1118 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js @@ -11,7 +11,7 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import { store as blockEditorStore } from '../../../store'; -import { PATTERN_TYPES } from '../block-patterns-tab/utils'; +import { INSERTER_PATTERN_TYPES } from '../block-patterns-tab/utils'; /** * Retrieves the block patterns inserter state. @@ -58,7 +58,7 @@ const usePatternsState = ( onInsert, rootClientId ) => { const onClickPattern = useCallback( ( pattern, blocks ) => { const patternBlocks = - pattern.type === PATTERN_TYPES.user && + pattern.type === INSERTER_PATTERN_TYPES.user && pattern.syncStatus !== 'unsynced' ? [ createBlock( 'core/block', { ref: pattern.id } ) ] : blocks; diff --git a/packages/block-editor/src/components/inserter/index.js b/packages/block-editor/src/components/inserter/index.js index c048ee7b3b138c..0fdedfca89e7d8 100644 --- a/packages/block-editor/src/components/inserter/index.js +++ b/packages/block-editor/src/components/inserter/index.js @@ -186,7 +186,6 @@ class PrivateInserter extends Component { clientId={ clientId } isAppender={ isAppender } showInserterHelpPanel={ showInserterHelpPanel } - prioritizePatterns={ prioritizePatterns } /> ); } diff --git a/packages/block-editor/src/components/inserter/library.js b/packages/block-editor/src/components/inserter/library.js index 3a814638c2f48e..7d462b343c310d 100644 --- a/packages/block-editor/src/components/inserter/library.js +++ b/packages/block-editor/src/components/inserter/library.js @@ -26,18 +26,13 @@ function InserterLibrary( }, ref ) { - const { destinationRootClientId, prioritizePatterns } = useSelect( + const { destinationRootClientId } = useSelect( ( select ) => { - const { getBlockRootClientId, getSettings } = - select( blockEditorStore ); - + const { getBlockRootClientId } = select( blockEditorStore ); const _rootClientId = rootClientId || getBlockRootClientId( clientId ) || undefined; return { destinationRootClientId: _rootClientId, - prioritizePatterns: - getSettings().__experimentalPreferPatternsOnRoot && - ! _rootClientId, }; }, [ clientId, rootClientId ] @@ -54,7 +49,6 @@ function InserterLibrary( __experimentalInsertionIndex={ __experimentalInsertionIndex } __experimentalFilterValue={ __experimentalFilterValue } shouldFocusBlock={ shouldFocusBlock } - prioritizePatterns={ prioritizePatterns } ref={ ref } /> ); diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 24c099869ae0d6..6a38e52cbffba1 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -45,7 +45,6 @@ function InserterMenu( showMostUsedBlocks, __experimentalFilterValue = '', shouldFocusBlock = true, - prioritizePatterns, }, ref ) { @@ -258,7 +257,6 @@ function InserterMenu( diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js index 27a1c3f944f662..4795c3ce4fdc24 100644 --- a/packages/block-editor/src/components/inserter/tabs.js +++ b/packages/block-editor/src/components/inserter/tabs.js @@ -32,13 +32,11 @@ function InserterTabs( { showPatterns = false, showMedia = false, onSelect, - prioritizePatterns = false, tabsContents, } ) { const tabs = [ - prioritizePatterns && showPatterns && patternsTab, blocksTab, - ! prioritizePatterns && showPatterns && patternsTab, + showPatterns && patternsTab, showMedia && mediaTab, ].filter( Boolean ); diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index a90bf116e1d085..d756f28e43127f 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -63,7 +63,7 @@ function ListViewBlock( { const [ isHovered, setIsHovered ] = useState( false ); const [ settingsAnchorRect, setSettingsAnchorRect ] = useState(); - const { isLocked, canEdit } = useBlockLock( clientId ); + const { isLocked, canEdit, canMove } = useBlockLock( clientId ); const isFirstSelectedBlock = isSelected && selectedClientIds[ 0 ] === clientId; @@ -269,6 +269,7 @@ function ListViewBlock( { 'is-dragging': isDragged, 'has-single-cell': ! showBlockActions, 'is-synced': blockInformation?.isSynced, + 'is-draggable': canMove, } ); // Only include all selected blocks if the currently clicked on block diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 7afa7a1c984318..42423328d19e4f 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -15,6 +15,11 @@ // Use position relative for row animation. position: relative; + &.is-draggable, + &.is-draggable .block-editor-list-view-block-contents { + cursor: grab; + } + .block-editor-list-view-block-select-button { // When a row is expanded, retain the dark color. &[aria-expanded="true"] { @@ -378,6 +383,7 @@ height: $icon-size; margin-left: $grid-unit-05; width: $icon-size; + cursor: pointer; } // First level of indentation is aria-level 2, max indent is 8. diff --git a/packages/block-editor/src/components/media-upload-progress/constants.js b/packages/block-editor/src/components/media-upload-progress/constants.js new file mode 100644 index 00000000000000..4003cd30e44c53 --- /dev/null +++ b/packages/block-editor/src/components/media-upload-progress/constants.js @@ -0,0 +1,6 @@ +export const MEDIA_UPLOAD_STATE_IDLE = 0; +export const MEDIA_UPLOAD_STATE_UPLOADING = 1; +export const MEDIA_UPLOAD_STATE_SUCCEEDED = 2; +export const MEDIA_UPLOAD_STATE_FAILED = 3; +export const MEDIA_UPLOAD_STATE_RESET = 4; +export const MEDIA_UPLOAD_STATE_PAUSED = 11; diff --git a/packages/block-editor/src/components/media-upload-progress/index.native.js b/packages/block-editor/src/components/media-upload-progress/index.native.js index b64b08eec09d8f..cb5a25d0bb8669 100644 --- a/packages/block-editor/src/components/media-upload-progress/index.native.js +++ b/packages/block-editor/src/components/media-upload-progress/index.native.js @@ -15,23 +15,28 @@ import { subscribeMediaUpload } from '@wordpress/react-native-bridge'; * Internal dependencies */ import styles from './styles.scss'; - -export const MEDIA_UPLOAD_STATE_UPLOADING = 1; -export const MEDIA_UPLOAD_STATE_SUCCEEDED = 2; -export const MEDIA_UPLOAD_STATE_FAILED = 3; -export const MEDIA_UPLOAD_STATE_RESET = 4; +import { + MEDIA_UPLOAD_STATE_IDLE, + MEDIA_UPLOAD_STATE_UPLOADING, + MEDIA_UPLOAD_STATE_SUCCEEDED, + MEDIA_UPLOAD_STATE_FAILED, + MEDIA_UPLOAD_STATE_RESET, + MEDIA_UPLOAD_STATE_PAUSED, +} from './constants'; export class MediaUploadProgress extends Component { constructor( props ) { super( props ); this.state = { + uploadState: MEDIA_UPLOAD_STATE_IDLE, progress: 0, isUploadInProgress: false, isUploadFailed: false, }; this.mediaUpload = this.mediaUpload.bind( this ); + this.getRetryMessage = this.getRetryMessage.bind( this ); } componentDidMount() { @@ -45,7 +50,11 @@ export class MediaUploadProgress extends Component { mediaUpload( payload ) { const { mediaId } = this.props; - if ( payload.mediaId !== mediaId ) { + if ( + payload.mediaId !== mediaId || + ( payload.state === this.state.uploadState && + payload.progress === this.state.progress ) + ) { return; } @@ -56,6 +65,9 @@ export class MediaUploadProgress extends Component { case MEDIA_UPLOAD_STATE_SUCCEEDED: this.finishMediaUploadWithSuccess( payload ); break; + case MEDIA_UPLOAD_STATE_PAUSED: + this.finishMediaUploadWithPause( payload ); + break; case MEDIA_UPLOAD_STATE_FAILED: this.finishMediaUploadWithFailure( payload ); break; @@ -68,6 +80,7 @@ export class MediaUploadProgress extends Component { updateMediaProgress( payload ) { this.setState( { progress: payload.progress, + uploadState: payload.state, isUploadInProgress: true, isUploadFailed: false, } ); @@ -77,21 +90,48 @@ export class MediaUploadProgress extends Component { } finishMediaUploadWithSuccess( payload ) { - this.setState( { isUploadInProgress: false } ); + this.setState( { + uploadState: payload.state, + isUploadInProgress: false, + } ); if ( this.props.onFinishMediaUploadWithSuccess ) { this.props.onFinishMediaUploadWithSuccess( payload ); } } + finishMediaUploadWithPause( payload ) { + if ( ! this.props.enablePausedUploads ) { + this.finishMediaUploadWithFailure( payload ); + return; + } + + this.setState( { + uploadState: payload.state, + isUploadInProgress: true, + isUploadFailed: false, + } ); + if ( this.props.onFinishMediaUploadWithFailure ) { + this.props.onFinishMediaUploadWithFailure( payload ); + } + } + finishMediaUploadWithFailure( payload ) { - this.setState( { isUploadInProgress: false, isUploadFailed: true } ); + this.setState( { + uploadState: payload.state, + isUploadInProgress: false, + isUploadFailed: true, + } ); if ( this.props.onFinishMediaUploadWithFailure ) { this.props.onFinishMediaUploadWithFailure( payload ); } } mediaUploadStateReset( payload ) { - this.setState( { isUploadInProgress: false, isUploadFailed: false } ); + this.setState( { + uploadState: payload.state, + isUploadInProgress: false, + isUploadFailed: false, + } ); if ( this.props.onMediaUploadStateReset ) { this.props.onMediaUploadStateReset( payload ); } @@ -115,15 +155,24 @@ export class MediaUploadProgress extends Component { } } + getRetryMessage() { + if ( + this.state.uploadState === MEDIA_UPLOAD_STATE_PAUSED && + this.props.enablePausedUploads + ) { + return __( 'Waiting for connection' ); + } + + // eslint-disable-next-line @wordpress/i18n-no-collapsible-whitespace + return __( 'Failed to insert media.\nTap for more info.' ); + } + render() { const { renderContent = () => null } = this.props; - const { isUploadInProgress, isUploadFailed } = this.state; + const { isUploadInProgress, isUploadFailed, uploadState } = this.state; const showSpinner = this.state.isUploadInProgress; const progress = this.state.progress * 100; - // eslint-disable-next-line @wordpress/i18n-no-collapsible-whitespace - const retryMessage = __( - 'Failed to insert media.\nTap for more info.' - ); + const retryMessage = this.getRetryMessage(); const progressBarStyle = [ styles.progressBar, @@ -149,6 +198,9 @@ export class MediaUploadProgress extends Component { ) } { renderContent( { + isUploadPaused: + uploadState === MEDIA_UPLOAD_STATE_PAUSED && + this.props.enablePausedUploads, isUploadInProgress, isUploadFailed, retryMessage, diff --git a/packages/block-editor/src/components/media-upload-progress/test/index.native.js b/packages/block-editor/src/components/media-upload-progress/test/index.native.js index 1185c9c35a8682..e5a6b460d94ef5 100644 --- a/packages/block-editor/src/components/media-upload-progress/test/index.native.js +++ b/packages/block-editor/src/components/media-upload-progress/test/index.native.js @@ -14,13 +14,13 @@ import { /** * Internal dependencies */ +import { MediaUploadProgress } from '../'; import { - MediaUploadProgress, MEDIA_UPLOAD_STATE_UPLOADING, MEDIA_UPLOAD_STATE_SUCCEEDED, MEDIA_UPLOAD_STATE_FAILED, MEDIA_UPLOAD_STATE_RESET, -} from '../'; +} from '../constants'; let uploadCallBack; subscribeMediaUpload.mockImplementation( ( callback ) => { diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index e97efb2a4b3910..b5212497b287e3 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -19,6 +19,7 @@ import { ESCAPE } from '@wordpress/keycodes'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; function hasOnlyToolbarItem( elements ) { const dataProp = 'toolbarItem'; @@ -169,7 +170,7 @@ function useToolbarFocus( { }; }, [ initialIndex, initialFocusOnMount, onIndexChange, toolbarRef ] ); - const { getLastFocus } = useSelect( blockEditorStore ); + const { getLastFocus } = unlock( useSelect( blockEditorStore ) ); /** * Handles returning focus to the block editor canvas when pressing escape. */ diff --git a/packages/block-editor/src/components/offline-status/index.native.js b/packages/block-editor/src/components/offline-status/index.native.js deleted file mode 100644 index 0447791e69a7e1..00000000000000 --- a/packages/block-editor/src/components/offline-status/index.native.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * External dependencies - */ -import { Text, View } from 'react-native'; - -/** - * WordPress dependencies - */ -import { - usePreferredColorSchemeStyle, - useNetworkConnectivity, -} from '@wordpress/compose'; -import { Icon } from '@wordpress/components'; -import { offline as offlineIcon } from '@wordpress/icons'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import styles from './style.native.scss'; - -const OfflineStatus = () => { - const { isConnected } = useNetworkConnectivity(); - - const containerStyle = usePreferredColorSchemeStyle( - styles.offline, - styles.offline__dark - ); - - const textStyle = usePreferredColorSchemeStyle( - styles[ 'offline--text' ], - styles[ 'offline--text__dark' ] - ); - - const iconStyle = usePreferredColorSchemeStyle( - styles[ 'offline--icon' ], - styles[ 'offline--icon__dark' ] - ); - - return ! isConnected ? ( - - - { __( 'Working Offline' ) } - - ) : null; -}; - -export default OfflineStatus; diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index f83d03ece47983..1f536011b35b6f 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -52,10 +52,6 @@ import { getFormatColors } from './get-format-colors'; import styles from './style.scss'; import ToolbarButtonWithOptions from './toolbar-button-with-options'; -const unescapeSpaces = ( text ) => { - return text.replace( / | /gi, ' ' ); -}; - // The flattened color palettes array is memoized to ensure that the same array instance is // returned for the colors palettes. This value might be used as a prop, so having the same // instance will prevent unnecessary re-renders of the RichText component. @@ -318,7 +314,7 @@ export class RichText extends Component { } const contentWithoutRootTag = this.removeRootTagsProducedByAztec( - unescapeSpaces( event.nativeEvent.text ) + event.nativeEvent.text ); // On iOS, onChange can be triggered after selection changes, even though there are no content changes. if ( contentWithoutRootTag === this.value.toString() ) { @@ -333,7 +329,7 @@ export class RichText extends Component { onTextUpdate( event ) { const contentWithoutRootTag = this.removeRootTagsProducedByAztec( - unescapeSpaces( event.nativeEvent.text ) + event.nativeEvent.text ); this.debounceCreateUndoLevel(); @@ -660,7 +656,7 @@ export class RichText extends Component { // Check and dicsard stray event, where the text and selection is equal to the ones already cached. const contentWithoutRootTag = this.removeRootTagsProducedByAztec( - unescapeSpaces( event.nativeEvent.text ) + event.nativeEvent.text ); if ( contentWithoutRootTag === this.value.toString() && diff --git a/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap b/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap index c9d3d62e40ce9d..84e9b467132714 100644 --- a/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap +++ b/packages/block-editor/src/components/rich-text/native/test/__snapshots__/index.native.js.snap @@ -1,12 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` Font Size renders component with style and font size 1`] = ` +exports[` when applying the font size renders component with style and font size 1`] = ` "

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed imperdiet ut nibh vitae ornare. Sed auctor nec augue at blandit.

" `; -exports[` Font Size should update the font size when style prop with font size property is provided 1`] = ` +exports[` when applying the font size should update the font size when style prop with font size property is provided 1`] = ` Font Size should update the font size when style prop with `; -exports[` Font Size should update the font size with decimals when style prop with font size property is provided 1`] = ` +exports[` when applying the font size should update the font size with decimals when style prop with font size property is provided 1`] = ` ', () => { } ); } ); - describe( 'when changes arrive from Aztec', () => { + describe( 'when the value changes', () => { it( 'should avoid updating attributes when values are equal', async () => { const handleChange = jest.fn(); - const defaultEmptyValue = new RichTextData(); - const screen = render( + const defaultEmptyValue = RichTextData.empty(); + render( ', () => { expect( handleChange ).not.toHaveBeenCalled(); } ); + + it( 'should preserve non-breaking space HTML entity', () => { + const onChange = jest.fn(); + const onSelectionChange = jest.fn(); + // The initial value is created using an HTML element to preserve + // the HTML entity. + const initialValue = RichTextData.fromHTMLElement( + __unstableCreateElement( document, ' ' ) + ); + render( + + ); + + // Trigger selection event with same text value as initial. + fireEvent( + screen.getByLabelText( /Text input/ ), + 'onSelectionChange', + 0, + 0, + initialValue.toString(), + { + nativeEvent: { + eventCount: 0, + target: undefined, + text: initialValue.toString(), + }, + } + ); + + expect( onChange ).not.toHaveBeenCalled(); + expect( onSelectionChange ).toHaveBeenCalled(); + } ); } ); - describe( 'Font Size', () => { + describe( 'when applying the font size', () => { it( 'should display rich text at the DEFAULT font size.', () => { // Arrange. const expectedFontSize = 16; @@ -259,7 +301,7 @@ describe( '', () => { const fontSize = '10'; const style = { fontSize: '12' }; // Act. - const screen = render( ); + render( ); screen.update( ); // Assert. expect( screen.toJSON() ).toMatchSnapshot(); @@ -281,7 +323,7 @@ describe( '', () => { const fontSize = '10'; const style = { fontSize: '12.56px' }; // Act. - const screen = render( ); + render( ); screen.update( ); // Assert. expect( screen.toJSON() ).toMatchSnapshot(); diff --git a/packages/block-editor/src/components/rich-text/use-mark-persistent.js b/packages/block-editor/src/components/rich-text/use-mark-persistent.js index 9a564dfb7f97e8..10e157452fbe22 100644 --- a/packages/block-editor/src/components/rich-text/use-mark-persistent.js +++ b/packages/block-editor/src/components/rich-text/use-mark-persistent.js @@ -11,8 +11,7 @@ import { store as blockEditorStore } from '../../store'; export function useMarkPersistent( { html, value } ) { const previousText = useRef(); - const hasActiveFormats = - value.activeFormats && !! value.activeFormats.length; + const hasActiveFormats = !! value.activeFormats?.length; const { __unstableMarkLastChangeAsPersistent } = useDispatch( blockEditorStore ); diff --git a/packages/block-editor/src/components/url-input/index.js b/packages/block-editor/src/components/url-input/index.js index 1451397ce68e5f..947c39abfd0c7d 100644 --- a/packages/block-editor/src/components/url-input/index.js +++ b/packages/block-editor/src/components/url-input/index.js @@ -66,7 +66,6 @@ class URLInput extends Component { this.state = { suggestions: [], showSuggestions: false, - isUpdatingSuggestions: false, suggestionsValue: null, selectedSuggestion: null, suggestionsListboxId: '', @@ -102,11 +101,7 @@ class URLInput extends Component { } // Update suggestions when the value changes. - if ( - prevProps.value !== value && - ! this.props.disableSuggestions && - ! this.state.isUpdatingSuggestions - ) { + if ( prevProps.value !== value && ! this.props.disableSuggestions ) { if ( value?.length ) { // If the new value is not empty we need to update with suggestions for it. this.updateSuggestions( value ); @@ -183,7 +178,6 @@ class URLInput extends Component { } this.setState( { - isUpdatingSuggestions: true, selectedSuggestion: null, loading: true, } ); @@ -203,7 +197,6 @@ class URLInput extends Component { this.setState( { suggestions, - isUpdatingSuggestions: false, suggestionsValue: value, loading: false, showSuggestions: !! suggestions.length, @@ -235,9 +228,15 @@ class URLInput extends Component { } this.setState( { - isUpdatingSuggestions: false, loading: false, } ); + } ) + .finally( () => { + // If this is the current promise then reset the reference + // to allow for checking if a new request is made. + if ( this.suggestionsRequest === request ) { + this.suggestionsRequest = null; + } } ); // Note that this assignment is handled *before* the async search request @@ -255,11 +254,12 @@ class URLInput extends Component { // When opening the link editor, if there's a value present, we want to load the suggestions pane with the results for this input search value // Don't re-run the suggestions on focus if there are already suggestions present (prevents searching again when tabbing between the input and buttons) + // or there is already a request in progress. if ( value && ! disableSuggestions && - ! this.state.isUpdatingSuggestions && - ! ( suggestions && suggestions.length ) + ! ( suggestions && suggestions.length ) && + this.suggestionsRequest === null ) { // Ensure the suggestions are updated with the current input value. this.updateSuggestions( value ); diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index 0ef1a83411026a..5013ae02f8a55e 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -26,40 +26,37 @@ import BlockIcon from '../block-icon'; import { store as blockEditorStore } from '../../store'; export const useTransformCommands = () => { - const { clientIds } = useSelect( ( select ) => { - const { getSelectedBlockClientIds } = select( blockEditorStore ); - const selectedBlockClientIds = getSelectedBlockClientIds(); - - return { - clientIds: selectedBlockClientIds, - }; - }, [] ); - const blocks = useSelect( - ( select ) => - select( blockEditorStore ).getBlocksByClientId( clientIds ), - [ clientIds ] - ); const { replaceBlocks, multiSelect } = useDispatch( blockEditorStore ); - const { possibleBlockTransformations, canRemove } = useSelect( - ( select ) => { + const { blocks, clientIds, canRemove, possibleBlockTransformations } = + useSelect( ( select ) => { const { getBlockRootClientId, getBlockTransformItems, + getSelectedBlockClientIds, + getBlocksByClientId, canRemoveBlocks, } = select( blockEditorStore ); + + const selectedBlockClientIds = getSelectedBlockClientIds(); + const selectedBlocks = getBlocksByClientId( + selectedBlockClientIds + ); const rootClientId = getBlockRootClientId( - Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds + selectedBlockClientIds[ 0 ] ); return { + blocks: selectedBlocks, + clientIds: selectedBlockClientIds, possibleBlockTransformations: getBlockTransformItems( - blocks, + selectedBlocks, + rootClientId + ), + canRemove: canRemoveBlocks( + selectedBlockClientIds, rootClientId ), - canRemove: canRemoveBlocks( clientIds, rootClientId ), }; - }, - [ clientIds, blocks ] - ); + }, [] ); const isTemplate = blocks.length === 1 && isTemplatePart( blocks[ 0 ] ); diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index c49af2f80fca22..701cc9f4f8451f 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -133,7 +133,6 @@ export function onBlockDrop( * A function that returns an event handler function for block-related file drop events. * * @param {string} targetRootClientId The root client id where the block(s) will be inserted. - * @param {number} targetBlockIndex The index where the block(s) will be inserted. * @param {Function} getSettings A function that gets the block editor settings. * @param {Function} updateBlockAttributes A function that updates a block's attributes. * @param {Function} canInsertBlockType A function that returns checks whether a block type can be inserted. @@ -143,7 +142,6 @@ export function onBlockDrop( */ export function onFilesDrop( targetRootClientId, - targetBlockIndex, getSettings, updateBlockAttributes, canInsertBlockType, @@ -175,17 +173,11 @@ export function onFilesDrop( /** * A function that returns an event handler function for block-related HTML drop events. * - * @param {string} targetRootClientId The root client id where the block(s) will be inserted. - * @param {number} targetBlockIndex The index where the block(s) will be inserted. * @param {Function} insertOrReplaceBlocks A function that inserts or replaces blocks. * * @return {Function} The event handler for a block-related HTML drop event. */ -export function onHTMLDrop( - targetRootClientId, - targetBlockIndex, - insertOrReplaceBlocks -) { +export function onHTMLDrop( insertOrReplaceBlocks ) { return ( HTML ) => { const blocks = pasteHandler( { HTML, mode: 'BLOCKS' } ); @@ -309,17 +301,12 @@ export default function useOnBlockDrop( ); const _onFilesDrop = onFilesDrop( targetRootClientId, - targetBlockIndex, getSettings, updateBlockAttributes, canInsertBlockType, insertOrReplaceBlocks ); - const _onHTMLDrop = onHTMLDrop( - targetRootClientId, - targetBlockIndex, - insertOrReplaceBlocks - ); + const _onHTMLDrop = onHTMLDrop( insertOrReplaceBlocks ); return ( event ) => { const files = getFilesFromDataTransfer( event.dataTransfer ); diff --git a/packages/block-editor/src/components/use-on-block-drop/test/index.js b/packages/block-editor/src/components/use-on-block-drop/test/index.js index 03c873bf6a8fd7..cd4ef2ad4a8894 100644 --- a/packages/block-editor/src/components/use-on-block-drop/test/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/test/index.js @@ -306,12 +306,10 @@ describe( 'onFilesDrop', () => { const canInsertBlockType = noop; const insertOrReplaceBlocks = jest.fn(); const targetRootClientId = '1'; - const targetBlockIndex = 0; const getSettings = jest.fn( () => ( {} ) ); const onFileDropHandler = onFilesDrop( targetRootClientId, - targetBlockIndex, getSettings, updateBlockAttributes, canInsertBlockType, @@ -331,14 +329,12 @@ describe( 'onFilesDrop', () => { const insertOrReplaceBlocks = jest.fn(); const canInsertBlockType = noop; const targetRootClientId = '1'; - const targetBlockIndex = 0; const getSettings = jest.fn( () => ( { mediaUpload: true, } ) ); const onFileDropHandler = onFilesDrop( targetRootClientId, - targetBlockIndex, getSettings, updateBlockAttributes, canInsertBlockType, @@ -361,14 +357,12 @@ describe( 'onFilesDrop', () => { const canInsertBlockType = noop; const insertOrReplaceBlocks = jest.fn(); const targetRootClientId = '1'; - const targetBlockIndex = 0; const getSettings = jest.fn( () => ( { mediaUpload: true, } ) ); const onFileDropHandler = onFilesDrop( targetRootClientId, - targetBlockIndex, getSettings, updateBlockAttributes, canInsertBlockType, @@ -389,15 +383,9 @@ describe( 'onFilesDrop', () => { describe( 'onHTMLDrop', () => { it( 'does nothing if the HTML cannot be converted into blocks', () => { pasteHandler.mockImplementation( () => [] ); - const targetRootClientId = '1'; - const targetBlockIndex = 0; const insertOrReplaceBlocks = jest.fn(); - const eventHandler = onHTMLDrop( - targetRootClientId, - targetBlockIndex, - insertOrReplaceBlocks - ); + const eventHandler = onHTMLDrop( insertOrReplaceBlocks ); eventHandler(); expect( insertOrReplaceBlocks ).not.toHaveBeenCalled(); @@ -406,15 +394,9 @@ describe( 'onHTMLDrop', () => { it( 'inserts blocks if the HTML can be converted into blocks', () => { const blocks = [ 'blocks' ]; pasteHandler.mockImplementation( () => blocks ); - const targetRootClientId = '1'; - const targetBlockIndex = 0; const insertOrReplaceBlocks = jest.fn(); - const eventHandler = onHTMLDrop( - targetRootClientId, - targetBlockIndex, - insertOrReplaceBlocks - ); + const eventHandler = onHTMLDrop( insertOrReplaceBlocks ); eventHandler(); expect( insertOrReplaceBlocks ).toHaveBeenCalledWith( blocks ); diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index b1fb1800a53ea2..bfc64dde071533 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -12,6 +12,7 @@ import { useRef } from '@wordpress/element'; */ import { store as blockEditorStore } from '../../store'; import { isInSameBlock, isInsideRootBlock } from '../../utils/dom'; +import { unlock } from '../../lock-unlock'; export default function useTabNav() { const container = useRef(); @@ -20,16 +21,15 @@ export default function useTabNav() { const { hasMultiSelection, getSelectedBlockClientId, getBlockCount } = useSelect( blockEditorStore ); - const { setNavigationMode, setLastFocus } = useDispatch( blockEditorStore ); + const { setNavigationMode, setLastFocus } = unlock( + useDispatch( blockEditorStore ) + ); const isNavigationMode = useSelect( ( select ) => select( blockEditorStore ).isNavigationMode(), [] ); - const lastFocus = useSelect( - ( select ) => select( blockEditorStore ).getLastFocus(), - [] - ); + const { getLastFocus } = unlock( useSelect( blockEditorStore ) ); // Don't allow tabbing to this element in Navigation mode. const focusCaptureTabIndex = ! isNavigationMode ? '0' : undefined; @@ -45,7 +45,7 @@ export default function useTabNav() { } else if ( hasMultiSelection() ) { container.current.focus(); } else if ( getSelectedBlockClientId() ) { - lastFocus.current.focus(); + getLastFocus()?.current.focus(); } else { setNavigationMode( true ); @@ -163,7 +163,7 @@ export default function useTabNav() { } function onFocusOut( event ) { - setLastFocus( { ...lastFocus, current: event.target } ); + setLastFocus( { ...getLastFocus(), current: event.target } ); const { ownerDocument } = node; diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 67373ecd0516ca..d093d3da55c8d6 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -137,6 +137,17 @@ function resetBackgroundSize( style = {}, setAttributes ) { } ); } +/** + * Generates a CSS class name if an background image is set. + * + * @param {Object} style A block's style attribute. + * + * @return {string} CSS class name. + */ +export function getBackgroundImageClasses( style ) { + return hasBackgroundImageValue( style ) ? 'has-background' : ''; +} + function InspectorImagePreview( { label, filename, url: imgUrl } ) { const imgLabel = label || getFilename( imgUrl ); return ( diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 5767db829d1b37..0995f877309cc2 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -24,6 +24,7 @@ import { transformStyles, shouldSkipSerialization, } from './utils'; +import { getBackgroundImageClasses } from './background'; import { useSettings } from '../components/use-settings'; import InspectorControls from '../components/inspector-controls'; import { @@ -383,12 +384,27 @@ function useBlockProps( { )?.color; } - return addSaveProps( { style: extraStyles }, name, { + const saveProps = addSaveProps( { style: extraStyles }, name, { textColor, backgroundColor, gradient, style, } ); + + const hasBackgroundValue = + backgroundColor || + style?.color?.background || + gradient || + style?.color?.gradient; + + return { + ...saveProps, + className: classnames( + saveProps.className, + // Add background image classes in the editor, if not already handled by background color values. + ! hasBackgroundValue && getBackgroundImageClasses( style ) + ), + }; } export default { diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 54824558cb7036..a83d07398d54a9 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -351,7 +351,7 @@ function BlockWithLayoutStyles( { block: BlockListBlock, props } ) { const layoutClasses = useLayoutClasses( attributes, name ); const { kebabCase } = unlock( componentsPrivateApis ); - const selectorPrefix = `wp-container-${ kebabCase( name ) }-layout-`; + const selectorPrefix = `wp-container-${ kebabCase( name ) }-is-layout-`; // Higher specificity to override defaults from theme.json. const selector = `.${ selectorPrefix }${ id }.${ selectorPrefix }${ id }`; const [ blockGapSupport ] = useSettings( 'spacing.blockGap' ); diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index b21436161cb8c3..da9beb0ba73a95 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1919,18 +1919,3 @@ export function unsetBlockEditingMode( clientId = '' ) { clientId, }; } - -/** - * Action that sets the element that had focus when focus leaves the editor canvas. - * - * @param {Object} lastFocus The last focused element. - * - * - * @return {Object} Action object. - */ -export function setLastFocus( lastFocus = null ) { - return { - type: 'LAST_FOCUS', - lastFocus, - }; -} diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index 48c5d15d469be4..43c392bc7ce8cf 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -323,3 +323,18 @@ export function syncDerivedUpdates( callback ) { } ); }; } + +/** + * Action that sets the element that had focus when focus leaves the editor canvas. + * + * @param {Object} lastFocus The last focused element. + * + * + * @return {Object} Action object. + */ +export function setLastFocus( lastFocus = null ) { + return { + type: 'LAST_FOCUS', + lastFocus, + }; +} diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 98a75122f47245..d31a710fd94fe3 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -281,3 +281,14 @@ export const hasAllowedPatterns = createSelector( ), ] ); + +/** + * Returns the element of the last element that had focus when focus left the editor canvas. + * + * @param {Object} state Block editor state. + * + * @return {Object} Element. + */ +export function getLastFocus( state ) { + return state.lastFocus; +} diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index b6d455333c7a52..55d157c6927a2d 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2742,8 +2742,11 @@ export const __unstableGetContentLockingParent = createSelector( while ( state.blocks.parents.has( current ) ) { current = state.blocks.parents.get( current ); if ( - current && - getTemplateLock( state, current ) === 'contentOnly' + ( current && + getBlockName( state, current ) === 'core/block' && + window.__experimentalPatternPartialSyncing ) || + ( current && + getTemplateLock( state, current ) === 'contentOnly' ) ) { result = current; } @@ -2943,14 +2946,3 @@ export const isGroupable = createRegistrySelector( ); } ); - -/** - * Returns the element of the last element that had focus when focus left the editor canvas. - * - * @param {Object} state Block editor state. - * - * @return {Object} Element. - */ -export function getLastFocus( state ) { - return state.lastFocus; -} diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 0103b5192154c4..99281f55fcd093 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; +import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; const EMPTY_ARRAY = []; @@ -18,7 +18,7 @@ export function getUserPatterns( state ) { return { name: `core/block/${ userPattern.id }`, id: userPattern.id, - type: PATTERN_TYPES.user, + type: INSERTER_PATTERN_TYPES.user, title: userPattern.title.raw, categories: userPattern.wp_pattern_category.map( ( catId ) => categories && categories.get( catId ) diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 80489479724fff..2d7b1547394452 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -1,5 +1,6 @@ @import "./autocompleters/style.scss"; @import "./components/block-alignment-control/style.scss"; +@import "./components/block-canvas/style.scss"; @import "./components/block-icon/style.scss"; @import "./components/block-inspector/style.scss"; @import "./components/block-tools/style.scss"; diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index b9879932b148a7..d5079fbbb4ac2c 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 8.26.0 (2023-12-27) - ## 8.25.0 (2023-12-13) ## 8.24.0 (2023-11-29) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index c1f484ad9a9143..30e341d08923a0 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.26.0-prerelease", + "version": "8.25.0", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js index 773000ad7c1524..b50de773a42ce7 100644 --- a/packages/block-library/src/audio/edit.js +++ b/packages/block-library/src/audio/edit.js @@ -42,32 +42,19 @@ function AudioEdit( { className, setAttributes, onReplace, - isSelected, + isSelected: isSingleSelected, insertBlocksAfter, } ) { const { id, autoplay, loop, preload, src } = attributes; const isTemporaryAudio = ! id && isBlobURL( src ); - const { mediaUpload, multiAudioSelection } = useSelect( ( select ) => { - const { getSettings, getMultiSelectedBlockClientIds, getBlockName } = - select( blockEditorStore ); - const multiSelectedClientIds = getMultiSelectedBlockClientIds(); - - return { - mediaUpload: getSettings().mediaUpload, - multiAudioSelection: - multiSelectedClientIds.length && - multiSelectedClientIds.every( - ( _clientId ) => getBlockName( _clientId ) === 'core/audio' - ), - }; - }, [] ); + const { getSettings } = useSelect( blockEditorStore ); useEffect( () => { if ( ! id && isBlobURL( src ) ) { const file = getBlobByURL( src ); if ( file ) { - mediaUpload( { + getSettings().mediaUpload( { filesList: [ file ], onFileChange: ( [ media ] ) => onSelectAudio( media ), onError: ( e ) => onUploadError( e ), @@ -156,7 +143,7 @@ function AudioEdit( { return ( <> - { ! multiAudioSelection && ( + { isSingleSelected && ( + { isTemporaryAudio && } diff --git a/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap index dca3f782efc676..4cf28f7063ad31 100644 --- a/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap @@ -532,3 +532,15 @@ exports[`Audio block renders placeholder without crashing 1`] = ` `; + +exports[`Audio block should enable autoplay setting 1`] = ` +" +
+" +`; + +exports[`Audio block should enable loop setting 1`] = ` +" +
+" +`; diff --git a/packages/block-library/src/audio/test/edit.native.js b/packages/block-library/src/audio/test/edit.native.js index c191fd2fff7989..7296d595d7aaab 100644 --- a/packages/block-library/src/audio/test/edit.native.js +++ b/packages/block-library/src/audio/test/edit.native.js @@ -5,7 +5,10 @@ import { addBlock, dismissModal, fireEvent, + getBlock, + getEditorHtml, initializeEditor, + openBlockSettings, render, screen, setupCoreBlocks, @@ -31,6 +34,10 @@ jest.unmock( '@wordpress/react-native-aztec' ); const MEDIA_UPLOAD_STATE_FAILED = 3; +const AUDIO_BLOCK = ` +
+`; + let uploadCallBack; subscribeMediaUpload.mockImplementation( ( callback ) => { uploadCallBack = callback; @@ -100,4 +107,26 @@ describe( 'Audio block', () => { screen.getByText( 'Invalid URL. Audio file not found.' ) ).toBeVisible(); } ); + + it( 'should enable autoplay setting', async () => { + await initializeEditor( { initialHtml: AUDIO_BLOCK } ); + + const audioBlock = getBlock( screen, 'Audio' ); + fireEvent.press( audioBlock ); + await openBlockSettings( screen ); + + fireEvent.press( screen.getByText( 'Autoplay' ) ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'should enable loop setting', async () => { + await initializeEditor( { initialHtml: AUDIO_BLOCK } ); + + const audioBlock = getBlock( screen, 'Audio' ); + fireEvent.press( audioBlock ); + await openBlockSettings( screen ); + + fireEvent.press( screen.getByText( 'Loop' ) ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 67b2680f6840ed..57db2d166f9f99 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -8,12 +8,12 @@ import classnames from 'classnames'; */ import { useRegistry, useSelect, useDispatch } from '@wordpress/data'; import { useRef, useMemo, useEffect } from '@wordpress/element'; -import { useEntityProp, useEntityRecord } from '@wordpress/core-data'; +import { useEntityRecord, store as coreStore } from '@wordpress/core-data'; import { Placeholder, Spinner, - TextControl, - PanelBody, + ToolbarButton, + ToolbarGroup, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { @@ -21,13 +21,13 @@ import { __experimentalRecursionProvider as RecursionProvider, __experimentalUseHasRecursion as useHasRecursion, InnerBlocks, - InspectorControls, useBlockProps, Warning, privateApis as blockEditorPrivateApis, store as blockEditorStore, + BlockControls, } from '@wordpress/block-editor'; -import { getBlockSupport, parse } from '@wordpress/blocks'; +import { getBlockSupport, parse, cloneBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -97,9 +97,12 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { const attributes = getPartiallySyncedAttributes( block ); const newAttributes = { ...block.attributes }; for ( const attributeKey of attributes ) { - defaultValues[ blockId ] = block.attributes[ attributeKey ]; + defaultValues[ blockId ] ??= {}; + defaultValues[ blockId ][ attributeKey ] = + block.attributes[ attributeKey ]; if ( overrides[ blockId ] ) { - newAttributes[ attributeKey ] = overrides[ blockId ]; + newAttributes[ attributeKey ] = + overrides[ blockId ][ attributeKey ]; } } return { @@ -111,7 +114,7 @@ function applyInitialOverrides( blocks, overrides = {}, defaultValues ) { } function getOverridesFromBlocks( blocks, defaultValues ) { - /** @type {Record} */ + /** @type {Record>} */ const overrides = {}; for ( const block of blocks ) { Object.assign( @@ -123,15 +126,27 @@ function getOverridesFromBlocks( blocks, defaultValues ) { const attributes = getPartiallySyncedAttributes( block ); for ( const attributeKey of attributes ) { if ( - block.attributes[ attributeKey ] !== defaultValues[ blockId ] + block.attributes[ attributeKey ] !== + defaultValues[ blockId ][ attributeKey ] ) { - overrides[ blockId ] = block.attributes[ attributeKey ]; + overrides[ blockId ] ??= {}; + overrides[ blockId ][ attributeKey ] = + block.attributes[ attributeKey ]; } } } return Object.keys( overrides ).length > 0 ? overrides : undefined; } +function setBlockEditMode( setEditMode, blocks, mode ) { + blocks.forEach( ( block ) => { + const editMode = + mode || ( isPartiallySynced( block ) ? 'contentOnly' : 'disabled' ); + setEditMode( block.clientId, editMode ); + setBlockEditMode( setEditMode, block.innerBlocks, mode ); + } ); +} + export default function ReusableBlockEdit( { name, attributes: { ref, overrides }, @@ -149,18 +164,55 @@ export default function ReusableBlockEdit( { const isMissing = hasResolved && ! record; const initialOverrides = useRef( overrides ); const defaultValuesRef = useRef( {} ); + const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent, setBlockEditingMode, } = useDispatch( blockEditorStore ); - const { getBlockEditingMode } = useSelect( blockEditorStore ); const { syncDerivedUpdates } = unlock( useDispatch( blockEditorStore ) ); + const { innerBlocks, userCanEdit, getBlockEditingMode, getPostLinkProps } = + useSelect( + ( select ) => { + const { canUser } = select( coreStore ); + const { + getBlocks, + getBlockEditingMode: editingMode, + getSettings, + } = select( blockEditorStore ); + const blocks = getBlocks( patternClientId ); + const canEdit = canUser( 'update', 'blocks', ref ); + + // For editing link to the site editor if the theme and user permissions support it. + return { + innerBlocks: blocks, + userCanEdit: canEdit, + getBlockEditingMode: editingMode, + getPostLinkProps: getSettings().getPostLinkProps, + }; + }, + [ patternClientId, ref ] + ); + + const editOriginalProps = getPostLinkProps + ? getPostLinkProps( { + postId: ref, + postType: 'wp_block', + canvas: 'edit', + } ) + : {}; + + useEffect( + () => setBlockEditMode( setBlockEditingMode, innerBlocks ), + [ innerBlocks, setBlockEditingMode ] + ); + // Apply the initial overrides from the pattern block to the inner blocks. useEffect( () => { const initialBlocks = - editedRecord.blocks ?? + // Clone the blocks to generate new client IDs. + editedRecord.blocks?.map( ( block ) => cloneBlock( block ) ) ?? ( editedRecord.content && typeof editedRecord.content !== 'function' ? parse( editedRecord.content ) : [] ); @@ -193,18 +245,6 @@ export default function ReusableBlockEdit( { syncDerivedUpdates, ] ); - const innerBlocks = useSelect( - ( select ) => select( blockEditorStore ).getBlocks( patternClientId ), - [ patternClientId ] - ); - - const [ title, setTitle ] = useEntityProp( - 'postType', - 'wp_block', - 'title', - ref - ); - const { alignment, layout } = useInferredLayout( innerBlocks, parentLayout @@ -220,6 +260,7 @@ export default function ReusableBlockEdit( { } ); const innerBlocksProps = useInnerBlocksProps( blockProps, { + templateLock: 'all', layout, renderAppender: innerBlocks?.length ? undefined @@ -247,6 +288,11 @@ export default function ReusableBlockEdit( { }, blockEditorStore ); }, [ syncDerivedUpdates, patternClientId, registry, setAttributes ] ); + const handleEditOriginal = ( event ) => { + setBlockEditMode( setBlockEditingMode, innerBlocks, 'default' ); + editOriginalProps.onClick( event ); + }; + let children = null; if ( hasAlreadyRendered ) { @@ -275,17 +321,18 @@ export default function ReusableBlockEdit( { return ( - - - - - + { userCanEdit && editOriginalProps && ( + + + + { __( 'Edit original' ) } + + + + ) } { children === null ? (
) : ( diff --git a/packages/block-library/src/button/style.scss b/packages/block-library/src/button/style.scss index d9efc928c5b1c7..f441152107973f 100644 --- a/packages/block-library/src/button/style.scss +++ b/packages/block-library/src/button/style.scss @@ -98,19 +98,19 @@ $blocks-block__margin: 0.5em; border-radius: 0 !important; } -.wp-block-button.is-style-outline > .wp-block-button__link, -.wp-block-button .wp-block-button__link.is-style-outline { +.wp-block-button:where(.is-style-outline) > .wp-block-button__link, +.wp-block-button .wp-block-button__link:where(.is-style-outline) { border: 2px solid currentColor; padding: 0.667em 1.333em; } -.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-text-color), -.wp-block-button .wp-block-button__link.is-style-outline:not(.has-text-color) { +.wp-block-button:where(.is-style-outline) > .wp-block-button__link:not(.has-text-color), +.wp-block-button .wp-block-button__link:where(.is-style-outline):not(.has-text-color) { color: currentColor; } -.wp-block-button.is-style-outline > .wp-block-button__link:not(.has-background), -.wp-block-button .wp-block-button__link.is-style-outline:not(.has-background) { +.wp-block-button:where(.is-style-outline) > .wp-block-button__link:not(.has-background), +.wp-block-button .wp-block-button__link:where(.is-style-outline):not(.has-background) { background-color: transparent; // background-image is required to overwrite a gradient background background-image: none; diff --git a/packages/block-library/src/file/edit.js b/packages/block-library/src/file/edit.js index de318aaa35e416..528a488039acfd 100644 --- a/packages/block-library/src/file/edit.js +++ b/packages/block-library/src/file/edit.js @@ -63,7 +63,6 @@ function ClipboardToolbarButton( { text, disabled } ) { function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { const { id, - fileId, fileName, href, textLinkHref, @@ -73,27 +72,26 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { displayPreview, previewHeight, } = attributes; - const { media, mediaUpload } = useSelect( + const { getSettings } = useSelect( blockEditorStore ); + const { media } = useSelect( ( select ) => ( { media: id === undefined ? undefined : select( coreStore ).getMedia( id ), - mediaUpload: select( blockEditorStore ).getSettings().mediaUpload, } ), [ id ] ); const { createErrorNotice } = useDispatch( noticesStore ); - const { toggleSelection, __unstableMarkNextChangeAsNotPersistent } = - useDispatch( blockEditorStore ); + const { toggleSelection } = useDispatch( blockEditorStore ); useEffect( () => { // Upload a file drag-and-dropped into the editor. if ( isBlobURL( href ) ) { const file = getBlobByURL( href ); - mediaUpload( { + getSettings().mediaUpload( { filesList: [ file ], onFileChange: ( [ newMedia ] ) => onSelectFile( newMedia ), onError: onUploadError, @@ -109,26 +107,21 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { } }, [] ); - useEffect( () => { - if ( ! fileId && href ) { - // Add a unique fileId to each file block. - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { fileId: `wp-block-file--media-${ clientId }` } ); - } - }, [ href, fileId, clientId ] ); - function onSelectFile( newMedia ) { - if ( newMedia && newMedia.url ) { - const isPdf = newMedia.url.endsWith( '.pdf' ); - setAttributes( { - href: newMedia.url, - fileName: newMedia.title, - textLinkHref: newMedia.url, - id: newMedia.id, - displayPreview: isPdf ? true : undefined, - previewHeight: isPdf ? 600 : undefined, - } ); + if ( ! newMedia || ! newMedia.url ) { + return; } + + const isPdf = newMedia.url.endsWith( '.pdf' ); + setAttributes( { + href: newMedia.url, + fileName: newMedia.title, + textLinkHref: newMedia.url, + id: newMedia.id, + displayPreview: isPdf ? true : undefined, + previewHeight: isPdf ? 600 : undefined, + fileId: `wp-block-file--media-${ clientId }`, + } ); } function onUploadError( message ) { diff --git a/packages/block-library/src/gallery/block.json b/packages/block-library/src/gallery/block.json index fad92aed59bf77..a5425c55381f94 100644 --- a/packages/block-library/src/gallery/block.json +++ b/packages/block-library/src/gallery/block.json @@ -80,6 +80,10 @@ "type": "boolean", "default": true }, + "randomOrder": { + "type": "boolean", + "default": false + }, "fixedHeight": { "type": "boolean", "default": true diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 371a13b1bf5ad2..4a646ce8362233 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -88,7 +88,8 @@ function GalleryEdit( props ) { onFocus, } = props; - const { columns, imageCrop, linkTarget, linkTo, sizeSlug } = attributes; + const { columns, imageCrop, randomOrder, linkTarget, linkTo, sizeSlug } = + attributes; const { __unstableMarkNextChangeAsNotPersistent, @@ -388,6 +389,10 @@ function GalleryEdit( props ) { : __( 'Thumbnails are not cropped.' ); } + function toggleRandomOrder() { + setAttributes( { randomOrder: ! randomOrder } ); + } + function toggleOpenInNewTab( openInNewTab ) { const newLinkTarget = openInNewTab ? '_blank' : undefined; setAttributes( { linkTarget: newLinkTarget } ); @@ -552,6 +557,12 @@ function GalleryEdit( props ) { onChange={ toggleImageCrop } help={ getImageCropHelp } /> + parseInt( id, 10 ) ); @@ -190,6 +190,7 @@ const transforms = { { columns: parseInt( columns, 10 ), linkTo, + randomOrder: orderby === 'rand', }, imageIds.map( ( imageId ) => createBlock( 'core/image', { id: imageId } ) diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 5c5308af5dfc3c..604fb8a7ce52b2 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -812,6 +812,7 @@ export class ImageEdit extends Component { { ! this.state.isCaptionSelected && getToolbarEditButton( openMediaOptions ) } diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 85cf3de57b1275..add8e5989ab7de 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -97,12 +97,6 @@ function block_core_image_get_lightbox_settings( $block ) { // Get the lightbox setting from the block attributes. if ( isset( $block['attrs']['lightbox'] ) ) { $lightbox_settings = $block['attrs']['lightbox']; - // If the lightbox setting is not set in the block attributes, - // check the legacy lightbox settings that are set using the - // `gutenberg_should_render_lightbox` filter. - // We can remove this elseif statement when the legacy lightbox settings are removed. - } elseif ( isset( $block['legacyLightboxSettings'] ) ) { - $lightbox_settings = $block['legacyLightboxSettings']; } if ( ! isset( $lightbox_settings ) ) { diff --git a/packages/block-library/src/media-text/media-container.native.js b/packages/block-library/src/media-text/media-container.native.js index dbc30dcf23e7aa..ec6e3999807c8e 100644 --- a/packages/block-library/src/media-text/media-container.native.js +++ b/packages/block-library/src/media-text/media-container.native.js @@ -170,7 +170,7 @@ class MediaContainer extends Component { mediaWidth, shouldStack, } = this.props; - const { isUploadFailed, retryMessage } = params; + const { isUploadFailed, isUploadPaused, retryMessage } = params; const focalPointValues = ! focalPoint ? IMAGE_DEFAULT_FOCAL_POINT : focalPoint; @@ -203,6 +203,7 @@ class MediaContainer extends Component { focalPoint={ imageFill && focalPointValues } isSelected={ isMediaSelected } isUploadFailed={ isUploadFailed } + isUploadPaused={ isUploadPaused } isUploadInProgress={ isUploadInProgress } onSelectMediaUploadOption={ this.onSelectMediaUploadOption @@ -340,6 +341,7 @@ class MediaContainer extends Component { { getMediaOptions() } selector combined with the :not(is-collapsed) selector + // as a way to target the class being added to the parent nav element. + :not(.is-collapsed) > & { + &:not(.is-menu-open) { + display: block; + width: 100%; + position: relative; + z-index: auto; + background-color: inherit; + + .wp-block-navigation__responsive-container-close { + display: none; } } @@ -686,10 +687,11 @@ button.wp-block-navigation-item__content { font-size: inherit; } - &:not(.always-shown) { - @include break-small { - display: none; - } + // When the menu is collapsed, the menu button is visible. + // We are using the > selector combined with the :not(is-collapsed) selector + // as a way to target the class being added to the parent nav element. + :not(.is-collapsed) > & { + display: none; } } diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index ba8e6d1a6683a4..d42832a1f8d02e 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -3,6 +3,11 @@ */ import { store, getContext, getElement } from '@wordpress/interactivity'; +/** + * Internal dependencies + */ +import { NAVIGATION_MOBILE_COLLAPSE } from './constants'; + const focusableSelectors = [ 'a[href]', 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', @@ -180,10 +185,31 @@ const { state, actions } = store( 'core/navigation', { focusFirstElement() { const { ref } = getElement(); if ( state.isMenuOpen ) { - ref.querySelector( - '.wp-block-navigation-item > *:first-child' - ).focus(); + const focusableElements = + ref.querySelectorAll( focusableSelectors ); + focusableElements?.[ 0 ]?.focus(); + } + }, + initNav() { + const context = getContext(); + const mediaQuery = window.matchMedia( + `(max-width: ${ NAVIGATION_MOBILE_COLLAPSE })` + ); + + // Run once to set the initial state. + context.isCollapsed = mediaQuery.matches; + + function handleCollapse( event ) { + context.isCollapsed = event.matches; } + + // Run on resize to update the state. + mediaQuery.addEventListener( 'change', handleCollapse ); + + // Remove the listener when the component is unmounted. + return () => { + mediaQuery.removeEventListener( 'change', handleCollapse ); + }; }, }, } ); diff --git a/packages/block-library/src/post-featured-image/block.json b/packages/block-library/src/post-featured-image/block.json index 34e3bd6b2325fa..4c4ba6919eaff6 100644 --- a/packages/block-library/src/post-featured-image/block.json +++ b/packages/block-library/src/post-featured-image/block.json @@ -51,6 +51,10 @@ }, "customGradient": { "type": "string" + }, + "useFirstImageFromPost": { + "type": "boolean", + "default": false } }, "usesContext": [ "postId", "postType", "queryId" ], diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index 843f1cf66cdfcd..26f3439964f90e 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -25,6 +25,7 @@ import { store as blockEditorStore, __experimentalUseBorderProps as useBorderProps, } from '@wordpress/block-editor'; +import { useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { upload } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; @@ -64,14 +65,44 @@ export default function PostFeaturedImageEdit( { sizeSlug, rel, linkTarget, + useFirstImageFromPost, } = attributes; - const [ featuredImage, setFeaturedImage ] = useEntityProp( + + const [ storedFeaturedImage, setFeaturedImage ] = useEntityProp( 'postType', postTypeSlug, 'featured_media', postId ); + // Fallback to post content if no featured image is set. + // This is needed for the "Use first image from post" option. + const [ postContent ] = useEntityProp( + 'postType', + postTypeSlug, + 'content', + postId + ); + + const featuredImage = useMemo( () => { + if ( storedFeaturedImage ) { + return storedFeaturedImage; + } + + if ( ! useFirstImageFromPost ) { + return; + } + + const imageOpener = + /).)*)?}\s+)?-->/.exec( + postContent + ); + const imageId = + imageOpener?.groups?.attrs && + JSON.parse( imageOpener.groups.attrs )?.id; + return imageId; + }, [ storedFeaturedImage, useFirstImageFromPost, postContent ] ); + const { media, postType, postPermalink } = useSelect( ( select ) => { const { getMedia, getPostType, getEditedEntityRecord } = diff --git a/packages/block-library/src/post-featured-image/index.php b/packages/block-library/src/post-featured-image/index.php index 4a7aa2f3d8ab94..9a1fd315bb9524 100644 --- a/packages/block-library/src/post-featured-image/index.php +++ b/packages/block-library/src/post-featured-image/index.php @@ -54,9 +54,40 @@ function render_block_core_post_featured_image( $attributes, $content, $block ) } $featured_image = get_the_post_thumbnail( $post_ID, $size_slug, $attr ); + + // Get the first image from the post. + if ( $attributes['useFirstImageFromPost'] && ! $featured_image ) { + $content_post = get_post( $post_ID ); + $content = $content_post->post_content; + $processor = new WP_HTML_Tag_Processor( $content ); + + /* + * Transfer the image tag from the post into a new text snippet. + * Because the HTML API doesn't currently expose a way to extract + * HTML substrings this is necessary as a workaround. Of note, this + * is different than directly extracting the IMG tag: + * - If there are duplicate attributes in the source there will only be one in the output. + * - If there are single-quoted or unquoted attributes they will be double-quoted in the output. + * - If there are named character references in the attribute values they may be replaced with their direct code points. E.g. `…` becomes `…`. + * In the future there will likely be a mechanism to copy snippets of HTML from + * one document into another, via the HTML Processor's `get_outer_html()` or + * equivalent. When that happens it would be appropriate to replace this custom + * code with that canonical code. + */ + if ( $processor->next_tag( 'img' ) ) { + $tag_html = new WP_HTML_Tag_Processor( '' ); + $tag_html->next_tag(); + foreach ( $processor->get_attribute_names_with_prefix( '' ) as $name ) { + $tag_html->set_attribute( $name, $processor->get_attribute( $name ) ); + } + $featured_image = $tag_html->get_updated_html(); + } + } + if ( ! $featured_image ) { return ''; } + if ( $is_link ) { $link_target = $attributes['linkTarget']; $rel = ! empty( $attributes['rel'] ) ? 'rel="' . esc_attr( $attributes['rel'] ) . '"' : ''; diff --git a/packages/block-library/src/search/block.json b/packages/block-library/src/search/block.json index 5669a9089d0e03..15531475adc9ac 100644 --- a/packages/block-library/src/search/block.json +++ b/packages/block-library/src/search/block.json @@ -43,10 +43,6 @@ "type": "object", "default": {} }, - "buttonBehavior": { - "type": "string", - "default": "expand-searchfield" - }, "isSearchFieldHidden": { "type": "boolean", "default": false diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index 2d39494c282392..0123bdfd565698 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -59,8 +59,6 @@ import { // button is placed inside wrapper. const DEFAULT_INNER_PADDING = '4px'; -const BUTTON_BEHAVIOR_EXPAND = 'expand-searchfield'; - export default function SearchEdit( { className, attributes, @@ -79,7 +77,6 @@ export default function SearchEdit( { buttonText, buttonPosition, buttonUseIcon, - buttonBehavior, isSearchFieldHidden, style, } = attributes; @@ -187,9 +184,6 @@ export default function SearchEdit( { buttonUseIcon && ! hasNoButton ? 'wp-block-search__icon-button' : undefined, - hasOnlyButton && BUTTON_BEHAVIOR_EXPAND === buttonBehavior - ? 'wp-block-search__button-behavior-expand' - : undefined, hasOnlyButton && isSearchFieldHidden ? 'wp-block-search__searchfield-hidden' : undefined @@ -325,7 +319,7 @@ export default function SearchEdit( { : borderProps.style ), }; const handleButtonClick = () => { - if ( hasOnlyButton && BUTTON_BEHAVIOR_EXPAND === buttonBehavior ) { + if ( hasOnlyButton ) { setAttributes( { isSearchFieldHidden: ! isSearchFieldHidden, } ); diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index f5391eefc8caca..ae6ddb1c4fb372 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -36,7 +36,6 @@ function render_block_core_search( $attributes, $content, $block ) { $show_button = ( ! empty( $attributes['buttonPosition'] ) && 'no-button' === $attributes['buttonPosition'] ) ? false : true; $button_position = $show_button ? $attributes['buttonPosition'] : null; $query_params = ( ! empty( $attributes['query'] ) ) ? $attributes['query'] : array(); - $button_behavior = ( ! empty( $attributes['buttonBehavior'] ) ) ? $attributes['buttonBehavior'] : 'default'; $button = ''; $query_params_markup = ''; $inline_styles = styles_for_block_core_search( $attributes ); @@ -78,7 +77,7 @@ function render_block_core_search( $attributes, $content, $block ) { $input->set_attribute( 'value', get_search_query() ); $input->set_attribute( 'placeholder', $attributes['placeholder'] ); - $is_expandable_searchfield = 'button-only' === $button_position && 'expand-searchfield' === $button_behavior; + $is_expandable_searchfield = 'button-only' === $button_position; if ( $is_expandable_searchfield ) { $input->set_attribute( 'data-wp-bind--aria-hidden', '!context.isSearchInputVisible' ); $input->set_attribute( 'data-wp-bind--tabindex', 'state.tabindex' ); @@ -154,7 +153,7 @@ function render_block_core_search( $attributes, $content, $block ) { if ( $button->next_tag() ) { $button->add_class( implode( ' ', $button_classes ) ); - if ( 'expand-searchfield' === $attributes['buttonBehavior'] && 'button-only' === $attributes['buttonPosition'] ) { + if ( 'button-only' === $attributes['buttonPosition'] ) { $button->set_attribute( 'data-wp-bind--aria-label', 'state.ariaLabel' ); $button->set_attribute( 'data-wp-bind--aria-controls', 'state.ariaControls' ); $button->set_attribute( 'data-wp-bind--aria-expanded', 'context.isSearchInputVisible' ); @@ -249,10 +248,7 @@ function classnames_for_block_core_search( $attributes ) { } if ( 'button-only' === $attributes['buttonPosition'] ) { - $classnames[] = 'wp-block-search__button-only'; - if ( ! empty( $attributes['buttonBehavior'] ) && 'expand-searchfield' === $attributes['buttonBehavior'] ) { - $classnames[] = 'wp-block-search__button-behavior-expand wp-block-search__searchfield-hidden'; - } + $classnames[] = 'wp-block-search__button-only wp-block-search__searchfield-hidden'; } } diff --git a/packages/block-library/src/search/style.scss b/packages/block-library/src/search/style.scss index b8a446721241b8..4e283530a0e277 100644 --- a/packages/block-library/src/search/style.scss +++ b/packages/block-library/src/search/style.scss @@ -62,35 +62,7 @@ $button-spacing-y: math.div($grid-unit-15, 2); // 6px .wp-block-search__button[aria-expanded="true"] { max-width: calc(100% - 100px); } -} - -// We are lowering the specificity so that the button element can override the rule for the button inside the search block. -:where(.wp-block-search__button-inside .wp-block-search__inside-wrapper) { - padding: $grid-unit-05; - border: 1px solid $gray-600; - box-sizing: border-box; - - .wp-block-search__input { - border-radius: 0; - border: none; - padding: 0 $grid-unit-05; - - &:focus { - outline: none; - } - } - - // For lower specificity. - :where(.wp-block-search__button) { - padding: $grid-unit-05 $grid-unit-10; - } -} - -.wp-block-search.aligncenter .wp-block-search__inside-wrapper { - margin: auto; -} -.wp-block-search__button-behavior-expand { .wp-block-search__inside-wrapper { transition-property: width; min-width: 0 !important; @@ -123,7 +95,33 @@ $button-spacing-y: math.div($grid-unit-15, 2); // 6px } } -.wp-block[data-align="right"] .wp-block-search__button-behavior-expand { +// We are lowering the specificity so that the button element can override the rule for the button inside the search block. +:where(.wp-block-search__button-inside .wp-block-search__inside-wrapper) { + padding: $grid-unit-05; + border: 1px solid $gray-600; + box-sizing: border-box; + + .wp-block-search__input { + border-radius: 0; + border: none; + padding: 0 $grid-unit-05; + + &:focus { + outline: none; + } + } + + // For lower specificity. + :where(.wp-block-search__button) { + padding: $grid-unit-05 $grid-unit-10; + } +} + +.wp-block-search.aligncenter .wp-block-search__inside-wrapper { + margin: auto; +} + +.wp-block[data-align="right"] .wp-block-search.wp-block-search__button-only { .wp-block-search__inside-wrapper { float: right; } diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js index cddfb5dfdc4fc4..0c146400cd74fb 100644 --- a/packages/block-library/src/site-logo/edit.js +++ b/packages/block-library/src/site-logo/edit.js @@ -412,7 +412,6 @@ export default function LogoEdit( { siteIconId, mediaItemData, isRequestingMediaItem, - mediaUpload, } = useSelect( ( select ) => { const { canUser, getEntityRecord, getEditedEntityRecord } = select( coreStore ); @@ -444,9 +443,9 @@ export default function LogoEdit( { mediaItemData: mediaItem, isRequestingMediaItem: _isRequestingMediaItem, siteIconId: _siteIconId, - mediaUpload: select( blockEditorStore ).getSettings().mediaUpload, }; }, [] ); + const { getSettings } = useSelect( blockEditorStore ); const { editEntityRecord } = useDispatch( coreStore ); @@ -511,8 +510,8 @@ export default function LogoEdit( { }; const onFilesDrop = ( filesList ) => { - mediaUpload( { - allowedTypes: [ 'image' ], + getSettings().mediaUpload( { + allowedTypes: ALLOWED_MEDIA_TYPES, filesList, onFileChange( [ image ] ) { if ( isBlobURL( image?.url ) ) { diff --git a/packages/block-library/src/table/editor.scss b/packages/block-library/src/table/editor.scss index 0367ed0a9c5d95..55652742a5ae9d 100644 --- a/packages/block-library/src/table/editor.scss +++ b/packages/block-library/src/table/editor.scss @@ -1,7 +1,4 @@ .wp-block-table { - // Remove default
style. - margin: 0; - .wp-block[data-align="left"] > &, .wp-block[data-align="right"] > &, .wp-block[data-align="center"] > & { diff --git a/packages/block-library/src/template-part/index.php b/packages/block-library/src/template-part/index.php index 26d90c51b5529c..0c97f88b98e386 100644 --- a/packages/block-library/src/template-part/index.php +++ b/packages/block-library/src/template-part/index.php @@ -70,6 +70,12 @@ function render_block_core_template_part( $attributes ) { if ( isset( $block_template->area ) ) { $area = $block_template->area; } + + // Needed for the `render_block_core_template_part_file` and `render_block_core_template_part_none` actions below. + $block_template_file = _get_block_template_file( 'wp_template_part', $attributes['slug'] ); + if ( $block_template_file ) { + $template_part_file_path = $block_template_file['path']; + } } if ( '' !== $content && null !== $content ) { diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js index db1fbb197126af..60c841b18a9318 100644 --- a/packages/block-library/src/video/edit.js +++ b/packages/block-library/src/video/edit.js @@ -63,7 +63,7 @@ const ALLOWED_MEDIA_TYPES = [ 'video' ]; const VIDEO_POSTER_ALLOWED_MEDIA_TYPES = [ 'image' ]; function VideoEdit( { - isSelected, + isSelected: isSingleSelected, attributes, className, setAttributes, @@ -75,26 +75,13 @@ function VideoEdit( { const posterImageButton = useRef(); const { id, controls, poster, src, tracks } = attributes; const isTemporaryVideo = ! id && isBlobURL( src ); - const { mediaUpload, multiVideoSelection } = useSelect( ( select ) => { - const { getSettings, getMultiSelectedBlockClientIds, getBlockName } = - select( blockEditorStore ); - const multiSelectedClientIds = getMultiSelectedBlockClientIds(); - - return { - mediaUpload: getSettings().mediaUpload, - multiVideoSelection: - multiSelectedClientIds.length && - multiSelectedClientIds.every( - ( _clientId ) => getBlockName( _clientId ) === 'core/video' - ), - }; - }, [] ); + const { getSettings } = useSelect( blockEditorStore ); useEffect( () => { if ( ! id && isBlobURL( src ) ) { const file = getBlobByURL( src ); if ( file ) { - mediaUpload( { + getSettings().mediaUpload( { filesList: [ file ], onFileChange: ( [ media ] ) => onSelectVideo( media ), onError: onUploadError, @@ -195,7 +182,7 @@ function VideoEdit( { return ( <> - { ! multiVideoSelection && ( + { isSingleSelected && ( <> +
diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index 1aed7da68e36e3..5e8e65ddea45bd 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.49.0 (2023-12-27) - ## 4.48.0 (2023-12-13) ## 4.47.0 (2023-11-29) diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index f4d324cf6c9a90..56a0c56269addc 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "4.49.0-prerelease", + "version": "4.48.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index 95ef1d7bda9104..81813855697518 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.49.0 (2023-12-27) - ## 4.48.0 (2023-12-13) ## 4.47.0 (2023-11-29) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 822868a5974182..cdd2884211afe4 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.49.0-prerelease", + "version": "4.48.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 401295acf816b6..0cce621f5b6cdb 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 12.26.0 (2023-12-27) - ## 12.25.0 (2023-12-13) ## 12.24.0 (2023-11-29) diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 355251c5d16dab..a2aff291dbda35 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "12.26.0-prerelease", + "version": "12.25.0", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/browserslist-config/CHANGELOG.md b/packages/browserslist-config/CHANGELOG.md index 26f8bfb4b88b74..451716f50edb96 100644 --- a/packages/browserslist-config/CHANGELOG.md +++ b/packages/browserslist-config/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 5.32.0 (2023-12-27) - ## 5.31.0 (2023-12-13) ## 5.30.0 (2023-11-29) diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index 6e039d02c0f92a..815fe6eba00fe8 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/browserslist-config", - "version": "5.32.0-prerelease", + "version": "5.31.0", "description": "WordPress Browserslist shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md index 4368a32d6fa7c4..8c697a90b20bee 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 0.20.0 (2023-12-27) - ## Enhancements - Support conditional commands and commands loaders using the "disabled" config. diff --git a/packages/commands/package.json b/packages/commands/package.json index ef3fa9f6bfbfd6..20fcea5c67b118 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "0.20.0-prerelease", + "version": "0.19.0", "description": "Handles the commands menu.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 1814a457e70fe1..c7c1a515a64cec 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,11 +2,10 @@ ## Unreleased -## 25.15.0 (2023-12-27) - ### Bug Fix -- `DropdownMenu V2 `: better fallback on browsers that don't support CSS subgrid([#57327](https://github.com/WordPress/gutenberg/pull/57327)). +- `NumberControl`: Make increment and decrement buttons keyboard accessible. ([#57402](https://github.com/WordPress/gutenberg/pull/57402)). +- `DropdownMenu V2`: better fallback on browsers that don't support CSS subgrid([#57327](https://github.com/WordPress/gutenberg/pull/57327)). - `FontSizePicker`: Use Button API for keeping focus on reset ([#57221](https://github.com/WordPress/gutenberg/pull/57221)). - `FontSizePicker`: Fix Reset button focus loss ([#57196](https://github.com/WordPress/gutenberg/pull/57196)). - `PaletteEdit`: Consider digits when generating kebab-cased slug ([#56713](https://github.com/WordPress/gutenberg/pull/56713)). @@ -17,15 +16,22 @@ - `Truncate`: improve handling of non-string `children` ([#57261](https://github.com/WordPress/gutenberg/pull/57261)). - `PaletteEdit`: Don't discard colors with default name and slug ([#54332](https://github.com/WordPress/gutenberg/pull/54332)). - `RadioControl`: Fully encapsulate styles ([#57347](https://github.com/WordPress/gutenberg/pull/57347)). +- `DuotonePicker`: Remove top margin when no duotone options ([#57489](https://github.com/WordPress/gutenberg/pull/57489)). +- `Snackbar`: Fix icon positioning ([#57377](https://github.com/WordPress/gutenberg/pull/57377)). - `GradientPicker`: Use slug while iterating over gradient entries to avoid React "duplicated key" warning ([#57361](https://github.com/WordPress/gutenberg/pull/57361)). +- `NavigatorProvider`: Exclude `size` value from `contain` CSS rule ([#57498](https://github.com/WordPress/gutenberg/pull/57498)). ### Enhancements - Update `ariakit` to version `0.3.10` ([#57325](https://github.com/WordPress/gutenberg/pull/57325)). +- Update `@ariakit/react` to version `0.3.12` and @ariakit/test to version `0.3.7` ([#57547](https://github.com/WordPress/gutenberg/pull/57547)). - `DropdownMenuV2`: do not collapse suffix width ([#57238](https://github.com/WordPress/gutenberg/pull/57238)). - `DateTimePicker`: Adjustment of the dot position on DayButton and expansion of the button area. ([#55502](https://github.com/WordPress/gutenberg/pull/55502)). - `Modal`: Improve application of body class names ([#55430](https://github.com/WordPress/gutenberg/pull/55430)). +- `BaseControl`: Connect to context system ([#57408](https://github.com/WordPress/gutenberg/pull/57408)). +- `InputControl`, `NumberControl`, `UnitControl`, `SelectControl`, `TreeSelect`: Add `compact` size variant ([#57398](https://github.com/WordPress/gutenberg/pull/57398)). - `ToggleGroupControl`: Update button size in large variant to be 32px ([#57338](https://github.com/WordPress/gutenberg/pull/57338)). +- `Tooltip`: improve unit tests ([#57345](https://github.com/WordPress/gutenberg/pull/57345)). ### Experimental diff --git a/packages/components/package.json b/packages/components/package.json index 5990dc552d7360..cd440998b93230 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "25.15.0-prerelease", + "version": "25.14.0", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -30,7 +30,7 @@ ], "types": "build-types", "dependencies": { - "@ariakit/react": "^0.3.10", + "@ariakit/react": "^0.3.12", "@babel/runtime": "^7.16.0", "@emotion/cache": "^11.7.1", "@emotion/css": "^11.7.1", @@ -88,4 +88,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/components/src/alignment-matrix-control/test/index.tsx b/packages/components/src/alignment-matrix-control/test/index.tsx index 6836bc7e45f95c..a820b69b26c8ff 100644 --- a/packages/components/src/alignment-matrix-control/test/index.tsx +++ b/packages/components/src/alignment-matrix-control/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { press, click } from '@ariakit/test'; /** * Internal dependencies @@ -37,11 +37,9 @@ describe( 'AlignmentMatrixControl', () => { } ); it( 'should be centered by default', async () => { - const user = userEvent.setup(); - await renderAndInitCompositeStore( ); - await user.tab(); + await press.Tab(); expect( getCell( 'center center' ) ).toHaveFocus(); } ); @@ -60,7 +58,6 @@ describe( 'AlignmentMatrixControl', () => { 'bottom center', 'bottom right', ] )( '%s', async ( alignment ) => { - const user = userEvent.setup(); const spy = jest.fn(); await renderAndInitCompositeStore( @@ -72,14 +69,13 @@ describe( 'AlignmentMatrixControl', () => { const cell = getCell( alignment ); - await user.click( cell ); + await click( cell ); expect( cell ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( alignment ); } ); it( 'unless already focused', async () => { - const user = userEvent.setup(); const spy = jest.fn(); await renderAndInitCompositeStore( @@ -91,7 +87,7 @@ describe( 'AlignmentMatrixControl', () => { const cell = getCell( 'center center' ); - await user.click( cell ); + await click( cell ); expect( cell ).toHaveFocus(); expect( spy ).not.toHaveBeenCalled(); @@ -106,16 +102,15 @@ describe( 'AlignmentMatrixControl', () => { [ 'ArrowLeft', 'center left' ], [ 'ArrowDown', 'bottom center' ], [ 'ArrowRight', 'center right' ], - ] )( '%s', async ( keyRef, cellRef ) => { - const user = userEvent.setup(); + ] as const )( '%s', async ( keyRef, cellRef ) => { const spy = jest.fn(); await renderAndInitCompositeStore( ); - await user.tab(); - await user.keyboard( `[${ keyRef }]` ); + await press.Tab(); + await press[ keyRef ](); expect( getCell( cellRef ) ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( cellRef ); @@ -128,8 +123,7 @@ describe( 'AlignmentMatrixControl', () => { [ 'ArrowLeft', 'top left' ], [ 'ArrowDown', 'bottom right' ], [ 'ArrowRight', 'bottom right' ], - ] )( '%s', async ( keyRef, cellRef ) => { - const user = userEvent.setup(); + ] as const )( '%s', async ( keyRef, cellRef ) => { const spy = jest.fn(); await renderAndInitCompositeStore( @@ -137,8 +131,8 @@ describe( 'AlignmentMatrixControl', () => { ); const cell = getCell( cellRef ); - await user.click( cell ); - await user.keyboard( `[${ keyRef }]` ); + await click( cell ); + await press[ keyRef ](); expect( cell ).toHaveFocus(); expect( spy ).toHaveBeenCalledWith( cellRef ); diff --git a/packages/components/src/base-control/index.tsx b/packages/components/src/base-control/index.tsx index 29251a114e0343..344643f28cfd7c 100644 --- a/packages/components/src/base-control/index.tsx +++ b/packages/components/src/base-control/index.tsx @@ -16,6 +16,7 @@ import { StyledVisualLabel, } from './styles/base-control-styles'; import type { WordPressComponentProps } from '../context'; +import { contextConnectWithoutRef, useContextSystem } from '../context'; export { useBaseControlProps } from './hooks'; @@ -42,19 +43,21 @@ export { useBaseControlProps } from './hooks'; * ); * ``` */ -export const BaseControl = ( { - __nextHasNoMarginBottom = false, - id, - label, - hideLabelFromVision = false, - help, - className, - children, -}: BaseControlProps ) => { +const UnconnectedBaseControl = ( + props: WordPressComponentProps< BaseControlProps, null > +) => { + const { + __nextHasNoMarginBottom = false, + id, + label, + hideLabelFromVision = false, + help, + className, + children, + } = useContextSystem( props, 'BaseControl' ); + return ( - + { label } ) : ( - - { label } - + { label } ) ) } { children } @@ -132,6 +133,10 @@ export const VisualLabel = ( { ); }; -BaseControl.VisualLabel = VisualLabel; + +export const BaseControl = Object.assign( + contextConnectWithoutRef( UnconnectedBaseControl, 'BaseControl' ), + { VisualLabel } +); export default BaseControl; diff --git a/packages/components/src/context/wordpress-component.ts b/packages/components/src/context/wordpress-component.ts index 57e69abdc38093..03c796bbbc3e40 100644 --- a/packages/components/src/context/wordpress-component.ts +++ b/packages/components/src/context/wordpress-component.ts @@ -8,14 +8,19 @@ export type WordPressComponentProps< /** Prop types. */ P, /** The HTML element to inherit props from. */ - T extends React.ElementType, + T extends React.ElementType | null, /** Supports polymorphism through the `as` prop. */ IsPolymorphic extends boolean = true, > = P & - // The `children` prop is being explicitly omitted since it is otherwise implicitly added - // by `ComponentPropsWithRef`. The context is that components should require the `children` - // prop explicitly when needed (see https://github.com/WordPress/gutenberg/pull/31817). - Omit< React.ComponentPropsWithoutRef< T >, 'as' | keyof P | 'children' > & + ( T extends React.ElementType + ? // The `children` prop is being explicitly omitted since it is otherwise implicitly added + // by `ComponentPropsWithRef`. The context is that components should require the `children` + // prop explicitly when needed (see https://github.com/WordPress/gutenberg/pull/31817). + Omit< + React.ComponentPropsWithoutRef< T >, + 'as' | keyof P | 'children' + > + : {} ) & ( IsPolymorphic extends true ? { /** The HTML element or React component to render the component as. */ @@ -24,7 +29,7 @@ export type WordPressComponentProps< : {} ); export type WordPressComponent< - T extends React.ElementType, + T extends React.ElementType | null, O, IsPolymorphic extends boolean, > = { diff --git a/packages/components/src/duotone-picker/duotone-picker.tsx b/packages/components/src/duotone-picker/duotone-picker.tsx index 759c517b4759e1..ee54c9cdf4235e 100644 --- a/packages/components/src/duotone-picker/duotone-picker.tsx +++ b/packages/components/src/duotone-picker/duotone-picker.tsx @@ -91,7 +91,7 @@ function DuotonePicker( { /> ); - const options = duotonePalette.map( ( { colors, slug, name } ) => { + const duotoneOptions = duotonePalette.map( ( { colors, slug, name } ) => { const style = { background: getGradientFromCSSColors( colors, '135deg' ), color: 'transparent', @@ -155,11 +155,15 @@ function DuotonePicker( { } } + const options = unsetable + ? [ unsetOption, ...duotoneOptions ] + : duotoneOptions; + return ( - + { ! disableCustomColors && ! disableCustomDuotone && ( { } ); }; -const fontSizeStyles = ( { inputSize: size }: InputProps ) => { +export const fontSizeStyles = ( { inputSize: size }: InputProps ) => { const sizes = { default: '13px', small: '11px', + compact: '13px', '__unstable-large': '13px', }; @@ -138,6 +139,13 @@ export const getSizeConfig = ( { paddingLeft: space( 2 ), paddingRight: space( 2 ), }, + compact: { + height: 32, + lineHeight: 1, + minHeight: 32, + paddingLeft: space( 2 ), + paddingRight: space( 2 ), + }, '__unstable-large': { height: 40, lineHeight: 1, @@ -148,13 +156,7 @@ export const getSizeConfig = ( { }; if ( ! __next40pxDefaultSize ) { - sizes.default = { - height: 32, - lineHeight: 1, - minHeight: 32, - paddingLeft: space( 2 ), - paddingRight: space( 2 ), - }; + sizes.default = sizes.compact; } return sizes[ size as Size ] || sizes.default; diff --git a/packages/components/src/input-control/types.ts b/packages/components/src/input-control/types.ts index a804efadd89d38..2056b69ae3e3dc 100644 --- a/packages/components/src/input-control/types.ts +++ b/packages/components/src/input-control/types.ts @@ -23,7 +23,7 @@ export type DragDirection = 'n' | 's' | 'e' | 'w'; export type DragProps = Parameters< Parameters< typeof useDrag >[ 0 ] >[ 0 ]; -export type Size = 'default' | 'small' | '__unstable-large'; +export type Size = 'default' | 'small' | 'compact' | '__unstable-large'; interface BaseProps { /** diff --git a/packages/components/src/mobile/image/constants.js b/packages/components/src/mobile/image/constants.js new file mode 100644 index 00000000000000..e866759bd2ac0d --- /dev/null +++ b/packages/components/src/mobile/image/constants.js @@ -0,0 +1 @@ +export const IMAGE_DEFAULT_FOCAL_POINT = { x: 0.5, y: 0.5 }; diff --git a/packages/components/src/mobile/image/index.native.js b/packages/components/src/mobile/image/index.native.js index 064150ec5f728d..09b21014a04b09 100644 --- a/packages/components/src/mobile/image/index.native.js +++ b/packages/components/src/mobile/image/index.native.js @@ -9,7 +9,7 @@ import FastImage from 'react-native-fast-image'; */ import { __ } from '@wordpress/i18n'; import { Icon } from '@wordpress/components'; -import { image as icon } from '@wordpress/icons'; +import { image, offline } from '@wordpress/icons'; import { usePreferredColorSchemeStyle } from '@wordpress/compose'; import { useEffect, useState, Platform } from '@wordpress/element'; @@ -22,13 +22,12 @@ import SvgIconRetry from './icon-retry'; import ImageEditingButton from './image-editing-button'; const ICON_TYPE = { + OFFLINE: 'offline', PLACEHOLDER: 'placeholder', RETRY: 'retry', UPLOAD: 'upload', }; -export const IMAGE_DEFAULT_FOCAL_POINT = { x: 0.5, y: 0.5 }; - const ImageComponent = ( { align, alt, @@ -39,6 +38,7 @@ const ImageComponent = ( { isSelected, shouldUseFastImage, isUploadFailed, + isUploadPaused, isUploadInProgress, mediaPickerOptions, onImageDataLoad, @@ -101,19 +101,23 @@ const ImageComponent = ( { }; const getIcon = ( iconType ) => { + let icon; let iconStyle; switch ( iconType ) { case ICON_TYPE.RETRY: - return ( - - ); + icon = retryIcon || SvgIconRetry; + iconStyle = iconRetryStyles; + break; + case ICON_TYPE.OFFLINE: + icon = offline; + iconStyle = iconOfflineStyles; + break; case ICON_TYPE.PLACEHOLDER: + icon = image; iconStyle = iconPlaceholderStyles; break; case ICON_TYPE.UPLOAD: + icon = image; iconStyle = iconUploadStyles; break; } @@ -130,6 +134,31 @@ const ImageComponent = ( { styles.iconUploadDark ); + const iconOfflineStyles = usePreferredColorSchemeStyle( + styles.iconOffline, + styles.iconOfflineDark + ); + + const retryIconStyles = usePreferredColorSchemeStyle( + styles.retryIcon, + styles.retryIconDark + ); + + const iconRetryStyles = usePreferredColorSchemeStyle( + styles.iconRetry, + styles.iconRetryDark + ); + + const retryContainerStyles = usePreferredColorSchemeStyle( + styles.retryContainer, + styles.retryContainerDark + ); + + const uploadFailedTextStyles = usePreferredColorSchemeStyle( + styles.uploadFailedText, + styles.uploadFailedTextDark + ); + const placeholderStyles = [ usePreferredColorSchemeStyle( styles.imageContainerUpload, @@ -216,9 +245,11 @@ const ImageComponent = ( { > { isSelected && highlightSelected && - ! ( isUploadInProgress || isUploadFailed ) && ( - - ) } + ! ( + isUploadInProgress || + isUploadFailed || + isUploadPaused + ) && } { ! imageData ? ( @@ -239,22 +270,24 @@ const ImageComponent = ( { ) } - { isUploadFailed && retryMessage && ( + { ( isUploadFailed || isUploadPaused ) && retryMessage && ( - { getIcon( ICON_TYPE.RETRY ) } + { isUploadPaused + ? getIcon( ICON_TYPE.OFFLINE ) + : getIcon( ICON_TYPE.RETRY ) } - + { retryMessage } @@ -265,7 +298,11 @@ const ImageComponent = ( { ) } diff --git a/packages/components/src/mobile/image/style.native.scss b/packages/components/src/mobile/image/style.native.scss index f6deb3655f3699..040a8e507667e8 100644 --- a/packages/components/src/mobile/image/style.native.scss +++ b/packages/components/src/mobile/image/style.native.scss @@ -21,10 +21,23 @@ } .retryIcon { - width: 80px; - height: 80px; - justify-content: center; - align-items: center; + background-color: $black; + border-radius: 200px; + padding: 8px; +} + +.retryIconDark { + background-color: $white; +} + +.iconOffline { + fill: $white; + width: 24px; + height: 24px; +} + +.iconOfflineDark { + fill: $black; } .customRetryIcon { @@ -33,9 +46,13 @@ } .iconRetry { - fill: #fff; - width: 100%; - height: 100%; + fill: $white; + width: 24px; + height: 24px; +} + +.iconRetryDark { + fill: $black; } .iconPlaceholder { @@ -90,12 +107,17 @@ } .uploadFailedText { - color: #fff; + color: $black; + font-weight: bold; font-size: 14; margin-top: 5; text-align: center; } +.uploadFailedTextDark { + color: $white; +} + .editContainer { width: 44px; height: 44px; @@ -116,7 +138,7 @@ } .iconCustomise { - fill: #fff; + fill: $white; position: absolute; top: 7px; left: 7px; @@ -124,6 +146,10 @@ .retryContainer { flex: 1; + background-color: "rgba(255, 255, 255, 0.8)"; +} + +.retryContainerDark { background-color: "rgba(0, 0, 0, 0.5)"; } diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js index eccb80f3903e5d..e66a0ffc28b542 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js @@ -2,26 +2,71 @@ * External dependencies */ import { FlatList } from 'react-native'; -import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated'; +import Animated from 'react-native-reanimated'; + +/** + * WordPress dependencies + */ +import { + forwardRef, + useCallback, + useImperativeHandle, +} from '@wordpress/element'; /** * Internal dependencies */ +import useScroll from './use-scroll'; import KeyboardAvoidingView from '../keyboard-avoiding-view'; const AnimatedFlatList = Animated.createAnimatedComponent( FlatList ); -export const KeyboardAwareFlatList = ( { innerRef, onScroll, ...props } ) => { - const scrollHandler = useAnimatedScrollHandler( { onScroll } ); +export const KeyboardAwareFlatList = ( { onScroll, ...props }, ref ) => { + const { extraScrollHeight, scrollEnabled, shouldPreventAutomaticScroll } = + props; + + const { + scrollViewRef, + scrollHandler, + scrollToSection, + scrollToElement, + onContentSizeChange, + lastScrollTo, + } = useScroll( { + scrollEnabled, + shouldPreventAutomaticScroll, + extraScrollHeight, + onScroll, + } ); + + const getFlatListRef = useCallback( + ( flatListRef ) => { + // On Android, we get the ref of the associated scroll + // view to the FlatList. + scrollViewRef.current = flatListRef?.getNativeScrollRef(); + }, + [ scrollViewRef ] + ); + + useImperativeHandle( ref, () => { + return { + scrollViewRef: scrollViewRef.current, + scrollToSection, + scrollToElement, + lastScrollTo, + }; + } ); + return ( ); }; -export default KeyboardAwareFlatList; +export default forwardRef( KeyboardAwareFlatList ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js index 8dc58008a3600d..ac2c89188cbf68 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -2,139 +2,104 @@ * External dependencies */ -import { ScrollView, FlatList, useWindowDimensions } from 'react-native'; -import Animated, { - useAnimatedScrollHandler, - useSharedValue, -} from 'react-native-reanimated'; +import { ScrollView, FlatList } from 'react-native'; +import Animated from 'react-native-reanimated'; /** * WordPress dependencies */ -import { useCallback, useEffect, useRef } from '@wordpress/element'; +import { + useCallback, + useEffect, + forwardRef, + useImperativeHandle, +} from '@wordpress/element'; import { useThrottle } from '@wordpress/compose'; /** * Internal dependencies */ +import useScroll from './use-scroll'; import useTextInputOffset from './use-text-input-offset'; -import useKeyboardOffset from './use-keyboard-offset'; -import useScrollToTextInput from './use-scroll-to-text-input'; import useTextInputCaretPosition from './use-text-input-caret-position'; +const DEFAULT_FONT_SIZE = 16; const AnimatedScrollView = Animated.createAnimatedComponent( ScrollView ); +/** @typedef {import('@wordpress/element').RefObject} RefObject */ /** * React component that provides a FlatList that is aware of the keyboard state and can scroll * to the currently focused TextInput. * - * @param {Object} props Component props. - * @param {number} props.extraScrollHeight Extra scroll height for the content. - * @param {Function} props.innerRef Function to pass the ScrollView ref to the parent component. - * @param {Function} props.onScroll Function to be called when the list is scrolled. - * @param {boolean} props.scrollEnabled Whether the list can be scrolled. - * @param {Object} props.scrollViewStyle Additional style for the ScrollView component. - * @param {boolean} props.shouldPreventAutomaticScroll Whether to prevent scrolling when there's a Keyboard offset set. - * @param {Object} props... Other props to pass to the FlatList component. + * @param {Object} props Component props. + * @param {number} props.extraScrollHeight Extra scroll height for the content. + * @param {Function} props.onScroll Function to be called when the list is scrolled. + * @param {boolean} props.scrollEnabled Whether the list can be scrolled. + * @param {Object} props.scrollViewStyle Additional style for the ScrollView component. + * @param {boolean} props.shouldPreventAutomaticScroll Whether to prevent scrolling when there's a Keyboard offset set. + * @param {Object} props... Other props to pass to the FlatList component. + * @param {RefObject} ref * @return {Component} KeyboardAwareFlatList component. */ -export const KeyboardAwareFlatList = ( { - extraScrollHeight, - innerRef, - onScroll, - scrollEnabled, - scrollViewStyle, - shouldPreventAutomaticScroll, - ...props -} ) => { - const scrollViewRef = useRef(); - const scrollViewMeasurements = useRef(); - const scrollViewYOffset = useSharedValue( -1 ); - - const { height: windowHeight, width: windowWidth } = useWindowDimensions(); - const isLandscape = windowWidth >= windowHeight; - - const [ keyboardOffset ] = useKeyboardOffset( +export const KeyboardAwareFlatList = ( + { + extraScrollHeight, + onScroll, scrollEnabled, - shouldPreventAutomaticScroll - ); - - const [ currentCaretData ] = useTextInputCaretPosition( scrollEnabled ); + scrollViewStyle, + shouldPreventAutomaticScroll, + ...props + }, + ref +) => { + const { + scrollViewRef, + scrollHandler, + keyboardOffset, + scrollToSection, + scrollToElement, + onContentSizeChange, + lastScrollTo, + } = useScroll( { + scrollEnabled, + shouldPreventAutomaticScroll, + extraScrollHeight, + onScroll, + onSizeChange, + } ); const [ getTextInputOffset ] = useTextInputOffset( scrollEnabled, scrollViewRef ); - const [ scrollToTextInputOffset ] = useScrollToTextInput( - extraScrollHeight, - keyboardOffset, - scrollEnabled, - scrollViewMeasurements, - scrollViewRef, - scrollViewYOffset - ); - const onScrollToTextInput = useThrottle( useCallback( async ( caret ) => { + const { caretHeight = DEFAULT_FONT_SIZE } = caret ?? {}; const textInputOffset = await getTextInputOffset( caret ); const hasTextInputOffset = textInputOffset !== null; if ( hasTextInputOffset ) { - scrollToTextInputOffset( caret, textInputOffset ); + scrollToSection( textInputOffset, caretHeight ); } }, - [ getTextInputOffset, scrollToTextInputOffset ] + [ getTextInputOffset, scrollToSection ] ), 200, { leading: false } ); - useEffect( () => { - onScrollToTextInput( currentCaretData ); - }, [ currentCaretData, onScrollToTextInput ] ); - - // When the orientation changes, the ScrollView measurements - // need to be re-calculated. - useEffect( () => { - scrollViewMeasurements.current = null; - }, [ isLandscape ] ); - - const scrollHandler = useAnimatedScrollHandler( { - onScroll: ( event ) => { - const { contentOffset } = event; - scrollViewYOffset.value = contentOffset.y; - onScroll( event ); - }, - } ); - - const measureScrollView = useCallback( () => { - if ( scrollViewRef.current ) { - const scrollRef = scrollViewRef.current.getNativeScrollRef(); + const [ currentCaretData ] = useTextInputCaretPosition( scrollEnabled ); - scrollRef.measureInWindow( ( _x, y, width, height ) => { - scrollViewMeasurements.current = { y, width, height }; - } ); - } - }, [] ); + const onSizeChange = useCallback( + () => onScrollToTextInput( currentCaretData ), + [ currentCaretData, onScrollToTextInput ] + ); - const onContentSizeChange = useCallback( () => { + useEffect( () => { onScrollToTextInput( currentCaretData ); - - // Sets the first values when the content size changes. - if ( ! scrollViewMeasurements.current ) { - measureScrollView(); - } - }, [ measureScrollView, onScrollToTextInput, currentCaretData ] ); - - const getRef = useCallback( - ( ref ) => { - scrollViewRef.current = ref; - innerRef( ref ); - }, - [ innerRef ] - ); + }, [ currentCaretData, onScrollToTextInput ] ); // Adds content insets when the keyboard is opened to have // extra padding at the bottom. @@ -142,6 +107,15 @@ export const KeyboardAwareFlatList = ( { const style = [ { flex: 1 }, scrollViewStyle ]; + useImperativeHandle( ref, () => { + return { + scrollViewRef: scrollViewRef.current, + scrollToSection, + scrollToElement, + lastScrollTo, + }; + } ); + return ( { - it( 'scrolls up to the current TextInput offset', () => { +describe( 'useScrollToSection', () => { + it( 'scrolls up to the section', () => { // Arrange - const currentCaretData = { caretHeight: 10 }; + const sectionY = 50; + const sectionHeight = 10; + const extraScrollHeight = 50; const keyboardOffset = 100; const scrollEnabled = true; const scrollViewRef = { current: { scrollTo: jest.fn() } }; const scrollViewMeasurements = { current: { height: 600 } }; const scrollViewYOffset = { value: 150 }; - const textInputOffset = 50; const { result } = renderHook( () => - useScrollToTextInput( + useScrollToSection( extraScrollHeight, keyboardOffset, scrollEnabled, @@ -33,28 +34,29 @@ describe( 'useScrollToTextInput', () => { ); // Act - result.current[ 0 ]( currentCaretData, textInputOffset ); + result.current[ 0 ]( sectionY, sectionHeight ); // Assert expect( scrollViewRef.current.scrollTo ).toHaveBeenCalledWith( { - y: textInputOffset, + y: sectionY, animated: true, } ); } ); - it( 'scrolls down to the current TextInput offset', () => { + it( 'scrolls down to the section', () => { // Arrange - const currentCaretData = { caretHeight: 10 }; + const sectionY = 750; + const sectionHeight = 10; + const extraScrollHeight = 50; const keyboardOffset = 100; const scrollEnabled = true; const scrollViewRef = { current: { scrollTo: jest.fn() } }; const scrollViewMeasurements = { current: { height: 600 } }; const scrollViewYOffset = { value: 250 }; - const textInputOffset = 750; const { result } = renderHook( () => - useScrollToTextInput( + useScrollToSection( extraScrollHeight, keyboardOffset, scrollEnabled, @@ -65,15 +67,13 @@ describe( 'useScrollToTextInput', () => { ); // Act - result.current[ 0 ]( currentCaretData, textInputOffset ); + result.current[ 0 ]( sectionY, sectionHeight ); // Assert const expectedYOffset = - textInputOffset - + sectionY - ( scrollViewMeasurements.current.height - - ( keyboardOffset + - extraScrollHeight + - currentCaretData.caretHeight ) ); + ( keyboardOffset + extraScrollHeight + sectionHeight ) ); expect( scrollViewRef.current.scrollTo ).toHaveBeenCalledWith( { y: expectedYOffset, animated: true, @@ -82,17 +82,18 @@ describe( 'useScrollToTextInput', () => { it( 'does not scroll when the ScrollView ref is not available', () => { // Arrange - const currentCaretData = { caretHeight: 10 }; + const sectionY = 50; + const sectionHeight = 10; + const extraScrollHeight = 50; const keyboardOffset = 100; const scrollEnabled = true; const scrollViewRef = { current: null }; const scrollViewMeasurements = { current: { height: 600 } }; const scrollViewYOffset = { value: 0 }; - const textInputOffset = 50; const { result } = renderHook( () => - useScrollToTextInput( + useScrollToSection( extraScrollHeight, keyboardOffset, scrollEnabled, @@ -103,7 +104,7 @@ describe( 'useScrollToTextInput', () => { ); // Act - result.current[ 0 ]( currentCaretData, textInputOffset ); + result.current[ 0 ]( sectionY, sectionHeight ); // Assert expect( scrollViewRef.current ).toBeNull(); @@ -111,17 +112,18 @@ describe( 'useScrollToTextInput', () => { it( 'does not scroll when the scroll is not enabled', () => { // Arrange - const currentCaretData = { caretHeight: 10 }; + const sectionY = 50; + const sectionHeight = 10; + const extraScrollHeight = 50; const keyboardOffset = 100; const scrollEnabled = false; const scrollViewRef = { current: { scrollTo: jest.fn() } }; const scrollViewMeasurements = { current: { height: 600 } }; const scrollViewYOffset = { value: 0 }; - const textInputOffset = 50; const { result } = renderHook( () => - useScrollToTextInput( + useScrollToSection( extraScrollHeight, keyboardOffset, scrollEnabled, @@ -132,7 +134,7 @@ describe( 'useScrollToTextInput', () => { ); // Act - result.current[ 0 ]( currentCaretData, textInputOffset ); + result.current[ 0 ]( sectionY, sectionHeight ); // Assert expect( scrollViewRef.current.scrollTo ).not.toHaveBeenCalled(); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll.native.js new file mode 100644 index 00000000000000..ab34bb59b3a4e1 --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/test/use-scroll.native.js @@ -0,0 +1,71 @@ +/** + * External dependencies + */ + +import { renderHook } from 'test/helpers'; + +/** + * Internal dependencies + */ +import useScroll from '../use-scroll'; + +// Mock Reanimated with default mock +jest.mock( 'react-native-reanimated', () => ( { + ...require( 'react-native-reanimated/mock' ), + useAnimatedScrollHandler: jest.fn( ( args ) => args ), +} ) ); + +describe( 'useScroll', () => { + it( 'scrolls using current scroll position', () => { + const sectionY = 50; + const sectionHeight = 10; + const scrollViewMeasurements = { x: 0, y: 0, width: 0, height: 600 }; + const extraScrollHeight = 50; + const scrollEnabled = true; + const shouldPreventAutomaticScroll = false; + + const scrollTo = jest.fn(); + const measureInWindow = jest.fn( ( callback ) => + callback( ...Object.values( scrollViewMeasurements ) ) + ); + const scrollRef = { scrollTo, measureInWindow }; + const onScroll = jest.fn(); + const onSizeChange = jest.fn(); + + const { result } = renderHook( () => + useScroll( { + scrollEnabled, + shouldPreventAutomaticScroll, + onScroll, + onSizeChange, + extraScrollHeight, + } ) + ); + const { + scrollViewRef, + onContentSizeChange, + scrollHandler, + scrollToSection, + } = result.current; + + // Assign ref + scrollViewRef.current = scrollRef; + + // Check content size changes + onContentSizeChange(); + expect( measureInWindow ).toHaveBeenCalled(); + expect( onSizeChange ).toHaveBeenCalled(); + + // Set up initial scroll offset + scrollHandler.onScroll( { contentOffset: { y: 150 } } ); + + // Scroll to section + scrollToSection( sectionY, sectionHeight ); + + // Assert + expect( scrollTo ).toHaveBeenCalledWith( { + y: sectionY, + animated: true, + } ); + } ); +} ); diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-element.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-element.native.js new file mode 100644 index 00000000000000..048b6815cdec29 --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-element.native.js @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { useCallback } from '@wordpress/element'; + +/** @typedef {import('@wordpress/element').RefObject} RefObject */ +/** + * Hook to scroll to a specified element by taking into account the Keyboard + * and the Header. + * + * @param {RefObject} scrollViewRef Scroll view reference. + * @param {Function} scrollToSection Function to scroll. + * @return {Function[]} Function to scroll to an element. + */ +export default function useScrollToElement( scrollViewRef, scrollToSection ) { + /** + * Function to scroll to an element. + * + * @param {RefObject} elementRef Ref of the element. + */ + const scrollToElement = useCallback( + ( elementRef ) => { + if ( ! scrollViewRef.current || ! elementRef ) { + return; + } + + elementRef.current.measureLayout( + scrollViewRef.current, + ( _x, y, _width, height ) => { + if ( height || y ) { + scrollToSection( Math.round( y ), height ); + } + }, + () => {} + ); + }, + [ scrollViewRef, scrollToSection ] + ); + + return [ scrollToElement ]; +} diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-section.native.js similarity index 56% rename from packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js rename to packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-section.native.js index 3bdaba837a60b3..e889e09972cf41 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-text-input.native.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll-to-section.native.js @@ -8,24 +8,21 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; */ import { useCallback } from '@wordpress/element'; -const DEFAULT_FONT_SIZE = 16; - /** @typedef {import('@wordpress/element').RefObject} RefObject */ /** @typedef {import('react-native-reanimated').SharedValue} SharedValue */ /** - * Hook to scroll to the currently focused TextInput - * depending on where the caret is placed taking into - * account the Keyboard and the Header. + * Hook to scroll to a specified section by taking into account the Keyboard + * and the Header. * * @param {number} extraScrollHeight Extra space to not overlap the content. * @param {number} keyboardOffset Keyboard space offset. * @param {boolean} scrollEnabled Whether the scroll is enabled or not. * @param {RefObject} scrollViewMeasurements ScrollView Layout measurements. - * @param {RefObject} scrollViewRef ScrollView reference. + * @param {RefObject} scrollViewRef Scroll view reference. * @param {SharedValue} scrollViewYOffset Current offset position of the ScrollView. - * @return {Function[]} Function to scroll to the current TextInput's offset. + * @return {Function[]} Function to scroll to a section. */ -export default function useScrollToTextInput( +export default function useScrollToSection( extraScrollHeight, keyboardOffset, scrollEnabled, @@ -37,33 +34,31 @@ export default function useScrollToTextInput( const insets = top + bottom; /** - * Function to scroll to the current TextInput's offset. + * Function to scroll to a section. * - * @param {Object} caret The caret position data of the currently focused TextInput. - * @param {number} caret.caretHeight The height of the caret. - * @param {number} textInputOffset The offset calculated with the caret's Y coordinate + the - * TextInput's Y coord or height value. + * @param {Object} section Section data to scroll to. + * @param {number} section.y Y-coordinate of of the section. + * @param {number} section.height Height of the section. */ - const scrollToTextInputOffset = useCallback( - ( caret, textInputOffset ) => { - const { caretHeight = DEFAULT_FONT_SIZE } = caret ?? {}; - + const scrollToSection = useCallback( + ( sectionY, sectionHeight ) => { if ( ! scrollViewRef.current || ! scrollEnabled || - ! scrollViewMeasurements.current + ! scrollViewMeasurements ) { return; } + const currentScrollViewYOffset = Math.max( 0, scrollViewYOffset.value ); - // Scroll up. - if ( textInputOffset < currentScrollViewYOffset ) { + // Scroll to the top of the section. + if ( sectionY < currentScrollViewYOffset ) { scrollViewRef.current.scrollTo( { - y: textInputOffset, + y: sectionY, animated: true, } ); return; @@ -72,7 +67,7 @@ export default function useScrollToTextInput( const availableScreenSpace = Math.abs( Math.floor( scrollViewMeasurements.current.height - - ( keyboardOffset + extraScrollHeight + caretHeight ) + ( keyboardOffset + extraScrollHeight + sectionHeight ) ) ); const maxOffset = Math.floor( @@ -80,12 +75,12 @@ export default function useScrollToTextInput( ); const isAtTheTop = - textInputOffset < scrollViewMeasurements.current.y + insets; + sectionY < scrollViewMeasurements.current.y + insets; - // Scroll down. - if ( textInputOffset > maxOffset && ! isAtTheTop ) { + // Scroll to the bottom of the section. + if ( sectionY > maxOffset && ! isAtTheTop ) { scrollViewRef.current.scrollTo( { - y: textInputOffset - availableScreenSpace, + y: sectionY - availableScreenSpace, animated: true, } ); } @@ -101,5 +96,5 @@ export default function useScrollToTextInput( ] ); - return [ scrollToTextInputOffset ]; + return [ scrollToSection ]; } diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll.native.js b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll.native.js new file mode 100644 index 00000000000000..faba873a9903be --- /dev/null +++ b/packages/components/src/mobile/keyboard-aware-flat-list/use-scroll.native.js @@ -0,0 +1,100 @@ +/** + * External dependencies + */ + +import { useWindowDimensions } from 'react-native'; +import { + useAnimatedScrollHandler, + useSharedValue, +} from 'react-native-reanimated'; + +/** + * WordPress dependencies + */ +import { useCallback, useEffect, useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import useKeyboardOffset from './use-keyboard-offset'; +import useScrollToSection from './use-scroll-to-section'; +import useScrollToElement from './use-scroll-to-element'; + +export default function useScroll( { + scrollEnabled, + shouldPreventAutomaticScroll, + onScroll, + onSizeChange, + extraScrollHeight, +} ) { + const scrollViewRef = useRef(); + const scrollViewMeasurements = useRef(); + const scrollViewYOffset = useSharedValue( -1 ); + const lastScrollTo = useRef( { + clientId: null, + } ); + + const { height: windowHeight, width: windowWidth } = useWindowDimensions(); + const isLandscape = windowWidth >= windowHeight; + + const [ keyboardOffset ] = useKeyboardOffset( + scrollEnabled, + shouldPreventAutomaticScroll + ); + + const scrollHandler = useAnimatedScrollHandler( { + onScroll: ( event ) => { + const { contentOffset } = event; + scrollViewYOffset.value = contentOffset.y; + onScroll( event ); + }, + } ); + + // When the orientation changes, the ScrollView measurements + // need to be re-calculated. + useEffect( () => { + scrollViewMeasurements.current = null; + }, [ isLandscape ] ); + + const [ scrollToSection ] = useScrollToSection( + extraScrollHeight, + keyboardOffset, + scrollEnabled, + scrollViewMeasurements, + scrollViewRef, + scrollViewYOffset + ); + const [ scrollToElement ] = useScrollToElement( + scrollViewRef, + scrollToSection + ); + + const measureScrollView = useCallback( () => { + if ( scrollViewRef.current ) { + scrollViewRef.current.measureInWindow( ( _x, y, width, height ) => { + scrollViewMeasurements.current = { y, width, height }; + } ); + } + }, [ scrollViewRef ] ); + + const onContentSizeChange = useCallback( () => { + if ( onSizeChange ) { + onSizeChange(); + } + + // Sets the first values when the content size changes. + if ( ! scrollViewMeasurements.current ) { + measureScrollView(); + } + }, [ measureScrollView, onSizeChange ] ); + + return { + scrollViewRef, + scrollHandler, + keyboardOffset, + scrollToSection, + scrollToElement, + onContentSizeChange, + lastScrollTo, + }; +} diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index b1bee51805f782..616539ed9b636f 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -209,7 +209,7 @@ function UnforwardedModal( if ( shouldCloseOnEsc && - event.code === 'Escape' && + ( event.code === 'Escape' || event.key === 'Escape' ) && ! event.defaultPrevented ) { event.preventDefault(); diff --git a/packages/components/src/navigator/styles.ts b/packages/components/src/navigator/styles.ts index 8ec5f11da16d3a..0203edbdf1816a 100644 --- a/packages/components/src/navigator/styles.ts +++ b/packages/components/src/navigator/styles.ts @@ -7,10 +7,10 @@ export const navigatorProviderWrapper = css` /* Prevents horizontal overflow while animating screen transitions */ overflow-x: hidden; /* Mark this subsection of the DOM as isolated, providing performance benefits - * by limiting calculations of layout, style, paint, size, or any combination - * to a DOM subtree rather than the entire page. + * by limiting calculations of layout, style and paint to a DOM subtree rather + * than the entire page. */ - contain: strict; + contain: content; `; const fadeInFromRight = keyframes( { diff --git a/packages/components/src/number-control/README.md b/packages/components/src/number-control/README.md index 1a78cd25bb457c..3a54351cd29258 100644 --- a/packages/components/src/number-control/README.md +++ b/packages/components/src/number-control/README.md @@ -46,9 +46,9 @@ If `isDragEnabled` is true, this controls the amount of `px` to have been dragge ### spinControls - The type of spin controls to display. These are butons that allow the user to + The type of spin controls to display. These are buttons that allow the user to quickly increment and decrement the number. - + - 'none' - Do not show spin controls. - 'native' - Use browser's native HTML `input` controls. - 'custom' - Use plus and minus icon buttons. diff --git a/packages/components/src/number-control/index.tsx b/packages/components/src/number-control/index.tsx index 320ef4cb87d1da..57d811d69939b4 100644 --- a/packages/components/src/number-control/index.tsx +++ b/packages/components/src/number-control/index.tsx @@ -247,9 +247,7 @@ function UnforwardedNumberControl( className={ spinButtonClasses } icon={ plusIcon } size="small" - aria-hidden="true" - aria-label={ __( 'Increment' ) } - tabIndex={ -1 } + label={ __( 'Increment' ) } onClick={ buildSpinButtonClickHandler( 'up' ) } @@ -258,9 +256,7 @@ function UnforwardedNumberControl( className={ spinButtonClasses } icon={ resetIcon } size="small" - aria-hidden="true" - aria-label={ __( 'Decrement' ) } - tabIndex={ -1 } + label={ __( 'Decrement' ) } onClick={ buildSpinButtonClickHandler( 'down' ) } diff --git a/packages/components/src/number-control/types.ts b/packages/components/src/number-control/types.ts index 98ee8e0a672f08..8d198e777bd557 100644 --- a/packages/components/src/number-control/types.ts +++ b/packages/components/src/number-control/types.ts @@ -15,7 +15,7 @@ export type NumberControlProps = Omit< */ hideHTMLArrows?: boolean; /** - * The type of spin controls to display. These are butons that allow the + * The type of spin controls to display. These are buttons that allow the * user to quickly increment and decrement the number. * * - 'none' - Do not show spin controls. diff --git a/packages/components/src/select-control/styles/select-control-styles.ts b/packages/components/src/select-control/styles/select-control-styles.ts index d23c689e2901ee..f4b5dc920c04c1 100644 --- a/packages/components/src/select-control/styles/select-control-styles.ts +++ b/packages/components/src/select-control/styles/select-control-styles.ts @@ -11,6 +11,7 @@ import { COLORS, rtl } from '../../utils'; import { space } from '../../utils/space'; import type { SelectControlProps } from '../types'; import InputControlSuffixWrapper from '../../input-control/input-suffix-wrapper'; +import { fontSizeStyles } from '../../input-control/styles/input-control-styles'; interface SelectProps extends Pick< @@ -30,27 +31,6 @@ const disabledStyles = ( { disabled }: SelectProps ) => { } ); }; -const fontSizeStyles = ( { selectSize = 'default' }: SelectProps ) => { - const sizes = { - default: '13px', - small: '11px', - '__unstable-large': '13px', - }; - - const fontSize = sizes[ selectSize ]; - const fontSizeMobile = '16px'; - - if ( ! fontSize ) return ''; - - return css` - font-size: ${ fontSizeMobile }; - - @media ( min-width: 600px ) { - font-size: ${ fontSize }; - } - `; -}; - const sizeStyles = ( { __next40pxDefaultSize, multiple, @@ -75,6 +55,12 @@ const sizeStyles = ( { paddingTop: 0, paddingBottom: 0, }, + compact: { + height: 32, + minHeight: 32, + paddingTop: 0, + paddingBottom: 0, + }, '__unstable-large': { height: 40, minHeight: 40, @@ -84,12 +70,7 @@ const sizeStyles = ( { }; if ( ! __next40pxDefaultSize ) { - sizes.default = { - height: 32, - minHeight: 32, - paddingTop: 0, - paddingBottom: 0, - }; + sizes.default = sizes.compact; } const style = sizes[ selectSize ] || sizes.default; @@ -107,11 +88,12 @@ const sizePaddings = ( { const padding = { default: 16, small: 8, + compact: 8, '__unstable-large': 16, }; if ( ! __next40pxDefaultSize ) { - padding.default = 8; + padding.default = padding.compact; } const selectedPadding = padding[ selectSize ] || padding.default; diff --git a/packages/components/src/snackbar/stories/index.story.tsx b/packages/components/src/snackbar/stories/index.story.tsx index 953b33d273b3fc..c1f525b60b3919 100644 --- a/packages/components/src/snackbar/stories/index.story.tsx +++ b/packages/components/src/snackbar/stories/index.story.tsx @@ -3,9 +3,15 @@ */ import type { Meta, StoryFn } from '@storybook/react'; +/** + * WordPress dependencies + */ +import { wordpress } from '@wordpress/icons'; + /** * Internal dependencies */ +import Icon from '../../icon'; import Snackbar from '..'; const meta: Meta< typeof Snackbar > = { @@ -63,11 +69,7 @@ WithActions.args = { export const WithIcon: StoryFn< typeof Snackbar > = DefaultTemplate.bind( {} ); WithIcon.args = { children: 'Add an icon to make your snackbar stand out', - icon: ( - - 🌮 - - ), + icon: , }; export const WithExplicitDismiss: StoryFn< typeof Snackbar > = diff --git a/packages/components/src/snackbar/style.scss b/packages/components/src/snackbar/style.scss index 3cae9c8ef65d49..0ba1774d67382f 100644 --- a/packages/components/src/snackbar/style.scss +++ b/packages/components/src/snackbar/style.scss @@ -29,13 +29,14 @@ } .components-snackbar__content-with-icon { - margin-left: $grid-unit-30; + position: relative; + padding-left: $icon-size; } .components-snackbar__icon { position: absolute; - top: 24px; - left: 28px; + left: $grid-unit-10 * -1; + top: calc((#{$icon-size - ($default-font-size * $default-line-height)}) / -2); } .components-snackbar__dismiss-button { diff --git a/packages/components/src/toggle-group-control/test/index.tsx b/packages/components/src/toggle-group-control/test/index.tsx index b54b5764d4e0ff..99a2dd8a00421c 100644 --- a/packages/components/src/toggle-group-control/test/index.tsx +++ b/packages/components/src/toggle-group-control/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { press, click, hover, sleep } from '@ariakit/test'; /** * WordPress dependencies @@ -19,8 +19,13 @@ import { ToggleGroupControlOption, ToggleGroupControlOptionIcon, } from '../index'; +import { TOOLTIP_DELAY } from '../../tooltip'; import type { ToggleGroupControlProps } from '../types'; -import cleanupTooltip from '../../tooltip/test/utils'; + +const hoverOutside = async () => { + await hover( document.body ); + await hover( document.body, { clientX: 10, clientY: 10 } ); +}; const ControlledToggleGroupControl = ( { value: valueProp, @@ -113,7 +118,6 @@ describe.each( [ } ); } ); it( 'should call onChange with proper value', async () => { - const user = userEvent.setup(); const mockOnChange = jest.fn(); render( @@ -126,13 +130,12 @@ describe.each( [ ); - await user.click( screen.getByRole( 'radio', { name: 'R' } ) ); + await click( screen.getByRole( 'radio', { name: 'R' } ) ); expect( mockOnChange ).toHaveBeenCalledWith( 'rigas' ); } ); it( 'should render tooltip where `showTooltip` === `true`', async () => { - const user = userEvent.setup(); render( { optionsWithTooltip } @@ -143,19 +146,26 @@ describe.each( [ 'Click for Delicious Gnocchi' ); - await user.hover( firstRadio ); + await hover( firstRadio ); - const tooltip = await screen.findByText( - 'Click for Delicious Gnocchi' - ); + const tooltip = await screen.findByRole( 'tooltip', { + name: 'Click for Delicious Gnocchi', + } ); await waitFor( () => expect( tooltip ).toBeVisible() ); - await cleanupTooltip( user ); + // hover outside of radio + await hoverOutside(); + + // Tooltip should hide + expect( + screen.queryByRole( 'tooltip', { + name: 'Click for Delicious Gnocchi', + } ) + ).not.toBeInTheDocument(); } ); it( 'should not render tooltip', async () => { - const user = userEvent.setup(); render( { optionsWithTooltip } @@ -166,19 +176,24 @@ describe.each( [ 'Click for Sumptuous Caponata' ); - await user.hover( secondRadio ); + await hover( secondRadio ); - await waitFor( () => - expect( - screen.queryByText( 'Click for Sumptuous Caponata' ) - ).not.toBeInTheDocument() - ); + // Tooltip shouldn't show + expect( + screen.queryByText( 'Click for Sumptuous Caponata' ) + ).not.toBeInTheDocument(); + + // Advance time by default delay + await sleep( TOOLTIP_DELAY ); + + // Tooltip shouldn't show. + expect( + screen.queryByText( 'Click for Sumptuous Caponata' ) + ).not.toBeInTheDocument(); } ); if ( mode === 'controlled' ) { it( 'should reset values correctly', async () => { - const user = userEvent.setup(); - render( { options } @@ -188,25 +203,23 @@ describe.each( [ const rigasOption = screen.getByRole( 'radio', { name: 'R' } ); const jackOption = screen.getByRole( 'radio', { name: 'J' } ); - await user.click( rigasOption ); + await click( rigasOption ); expect( jackOption ).not.toBeChecked(); expect( rigasOption ).toBeChecked(); - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( rigasOption ).not.toBeChecked(); expect( jackOption ).toBeChecked(); - await user.click( screen.getByRole( 'button', { name: 'Reset' } ) ); + await click( screen.getByRole( 'button', { name: 'Reset' } ) ); expect( rigasOption ).not.toBeChecked(); expect( jackOption ).not.toBeChecked(); } ); it( 'should update correctly when triggered by external updates', async () => { - const user = userEvent.setup(); - render( { it( 'should not be deselectable', async () => { const mockOnChange = jest.fn(); - const user = userEvent.setup(); render( { - const user = userEvent.setup(); - render( - - { options } - + <> + + { options } + + + ); const rigas = screen.getByRole( 'radio', { name: 'R', } ); - await user.tab(); + await press.Tab(); expect( rigas ).toHaveFocus(); - await user.tab(); + await press.Tab(); + // When in controlled mode, there is an additional "Reset" button. const expectedFocusTarget = mode === 'uncontrolled' - ? rigas.ownerDocument.body + ? screen.getByRole( 'button', { + name: 'After ToggleGroupControl', + } ) : screen.getByRole( 'button', { name: 'Reset' } ); expect( expectedFocusTarget ).toHaveFocus(); @@ -301,7 +317,6 @@ describe.each( [ describe( 'isDeselectable = true', () => { it( 'should be deselectable', async () => { const mockOnChange = jest.fn(); - const user = userEvent.setup(); render( ); - await user.click( + await click( screen.getByRole( 'button', { name: 'R', pressed: true, @@ -323,7 +338,7 @@ describe.each( [ expect( mockOnChange ).toHaveBeenCalledTimes( 1 ); expect( mockOnChange ).toHaveBeenLastCalledWith( undefined ); - await user.click( + await click( screen.getByRole( 'button', { name: 'R', pressed: false, @@ -334,15 +349,13 @@ describe.each( [ } ); it( 'should tab to the next option button', async () => { - const user = userEvent.setup(); - render( { options } ); - await user.tab(); + await press.Tab(); expect( screen.getByRole( 'button', { name: 'R', @@ -350,7 +363,7 @@ describe.each( [ } ) ).toHaveFocus(); - await user.tab(); + await press.Tab(); expect( screen.getByRole( 'button', { name: 'J', @@ -359,7 +372,7 @@ describe.each( [ ).toHaveFocus(); // Focus should not move with arrow keys - await user.keyboard( '{ArrowLeft}' ); + await press.ArrowLeft(); expect( screen.getByRole( 'button', { name: 'J', diff --git a/packages/components/src/tooltip/index.tsx b/packages/components/src/tooltip/index.tsx index 80407def54cd45..817d6d18812ee4 100644 --- a/packages/components/src/tooltip/index.tsx +++ b/packages/components/src/tooltip/index.tsx @@ -66,7 +66,7 @@ function Tooltip( props: TooltipProps ) { const tooltipStore = Ariakit.useTooltipStore( { placement: computedPlacement, - timeout: delay, + showTimeout: delay, } ); return ( diff --git a/packages/components/src/tooltip/test/index.tsx b/packages/components/src/tooltip/test/index.tsx index 4d58498e278d36..cbe144cfa53d4d 100644 --- a/packages/components/src/tooltip/test/index.tsx +++ b/packages/components/src/tooltip/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { press, hover, click, sleep } from '@ariakit/test'; /** * WordPress dependencies @@ -15,321 +15,425 @@ import { shortcutAriaLabel } from '@wordpress/keycodes'; import Button from '../../button'; import Modal from '../../modal'; import Tooltip, { TOOLTIP_DELAY } from '..'; -import cleanupTooltip from './utils/'; const props = { - children: , + children: , text: 'tooltip text', }; -describe( 'Tooltip', () => { - it( 'should not render the tooltip if multiple children are passed', async () => { - render( - // expected TS error since Tooltip cannot have more than one child element - // @ts-expect-error - - - - - ); - - expect( - screen.queryByRole( 'tooltip', { name: /tooltip text/i } ) - ).not.toBeInTheDocument(); - } ); +const expectTooltipToBeVisible = () => + expect( + screen.getByRole( 'tooltip', { name: 'tooltip text' } ) + ).toBeVisible(); - it( 'should not render the tooltip if there is no focus', () => { - render( ); +const expectTooltipToBeHidden = () => + expect( + screen.queryByRole( 'tooltip', { name: 'tooltip text' } ) + ).not.toBeInTheDocument(); - expect( - screen.getByRole( 'button', { name: /Button/i } ) - ).toBeVisible(); +const waitExpectTooltipToShow = async ( timeout = TOOLTIP_DELAY ) => + await waitFor( expectTooltipToBeVisible, { timeout } ); - expect( - screen.queryByRole( 'tooltip', { name: /tooltip text/i } ) - ).not.toBeInTheDocument(); - } ); +const waitExpectTooltipToHide = async () => + await waitFor( expectTooltipToBeHidden ); - it( 'should render the tooltip when focusing on the tooltip anchor via tab', async () => { - const user = userEvent.setup(); +const hoverOutside = async () => { + await hover( document.body ); + await hover( document.body, { clientX: 10, clientY: 10 } ); +}; - render( ); +describe( 'Tooltip', () => { + // Wait enough time to make sure that tooltips don't show immediately, ignoring + // the showTimeout delay. For more context, see: + // - https://github.com/WordPress/gutenberg/pull/57345#discussion_r1435167187 + // - https://ariakit.org/reference/tooltip-provider#skiptimeout + afterEach( async () => { + await sleep( 300 ); + } ); - await user.tab(); + describe( 'basic behavior', () => { + it( 'should not render the tooltip if multiple children are passed', async () => { + render( + // @ts-expect-error Tooltip cannot have more than one child element + + + + + ); - expect( - screen.getByRole( 'button', { name: /Button/i } ) - ).toHaveFocus(); + expect( + screen.getByRole( 'button', { name: 'First button' } ) + ).toBeVisible(); + expect( + screen.getByRole( 'button', { name: 'Second button' } ) + ).toBeVisible(); - expect( - await screen.findByRole( 'tooltip', { name: /tooltip text/i } ) - ).toBeVisible(); + await press.Tab(); - await cleanupTooltip( user ); - } ); + expectTooltipToBeHidden(); + } ); - it( 'should render the tooltip when the tooltip anchor is hovered', async () => { - const user = userEvent.setup(); + it( 'should associate the tooltip text with its anchor via the accessible description when visible', async () => { + render( ); - render( ); + // The anchor can not be found by querying for its description, + // since that is present only when the tooltip is visible + expect( + screen.queryByRole( 'button', { description: 'tooltip text' } ) + ).not.toBeInTheDocument(); + + // Hover the anchor. The tooltip shows and its text is used to describe + // the tooltip anchor + await hover( + screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ) + ); + expect( + await screen.findByRole( 'button', { + description: 'tooltip text', + } ) + ).toBeInTheDocument(); + + // Hover outside of the anchor, tooltip should hide + await hoverOutside(); + await waitExpectTooltipToHide(); + expect( + screen.queryByRole( 'button', { description: 'tooltip text' } ) + ).not.toBeInTheDocument(); + } ); + } ); - await user.hover( screen.getByRole( 'button', { name: /Button/i } ) ); + describe( 'keyboard focus', () => { + it( 'should not render the tooltip if there is no focus', () => { + render( ); - expect( - await screen.findByRole( 'tooltip', { name: /tooltip text/i } ) - ).toBeVisible(); + expect( + screen.getByRole( 'button', { name: 'Tooltip anchor' } ) + ).toBeVisible(); + + expectTooltipToBeHidden(); + } ); + + it( 'should show the tooltip when focusing on the tooltip anchor and hide it the anchor loses focus', async () => { + render( + <> + + + + ); + + // Focus the anchor, tooltip should show + await press.Tab(); + expect( + screen.getByRole( 'button', { name: 'Tooltip anchor' } ) + ).toHaveFocus(); + await waitExpectTooltipToShow(); - await cleanupTooltip( user ); + // Focus the other button, tooltip should hide + await press.Tab(); + expect( + screen.getByRole( 'button', { name: 'Focus me' } ) + ).toHaveFocus(); + await waitExpectTooltipToHide(); + } ); + + it( 'should show tooltip when focussing a disabled (but focussable) anchor button', async () => { + render( + <> + + + + + + ); + + const anchor = screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ); + + expect( anchor ).toBeVisible(); + expect( anchor ).toHaveAttribute( 'aria-disabled', 'true' ); + + // Focus anchor, tooltip should show + await press.Tab(); + expect( anchor ).toHaveFocus(); + await waitExpectTooltipToShow(); + + // Focus another button, tooltip should hide + await press.Tab(); + expect( + screen.getByRole( 'button', { + name: 'Focus me', + } ) + ).toHaveFocus(); + await waitExpectTooltipToHide(); + } ); } ); - it( 'should not show tooltip on focus as result of mouse click', async () => { - const user = userEvent.setup(); + describe( 'mouse hover', () => { + it( 'should show the tooltip when the tooltip anchor is hovered and hide it when the cursor stops hovering the anchor', async () => { + render( ); + + const anchor = screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ); + + expect( anchor ).toBeVisible(); + + // Hover over the anchor, tooltip should show + await hover( anchor ); + await waitExpectTooltipToShow(); + + // Hover outside of the anchor, tooltip should hide + await hoverOutside(); + await waitExpectTooltipToHide(); + } ); + + it( 'should show tooltip when hovering over a disabled (but focussable) anchor button', async () => { + render( + <> + + + + + + ); + + const anchor = screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ); + + expect( anchor ).toBeVisible(); + expect( anchor ).toHaveAttribute( 'aria-disabled', 'true' ); + + // Hover over the anchor, tooltip should show + await hover( anchor ); + await waitExpectTooltipToShow(); + + // Hover outside of the anchor, tooltip should hide + await hoverOutside(); + await waitExpectTooltipToHide(); + } ); + } ); - render( ); + describe( 'mouse click', () => { + it( 'should hide tooltip when the tooltip anchor is clicked', async () => { + render( ); - await user.click( screen.getByRole( 'button', { name: /Button/i } ) ); + const anchor = screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ); - expect( - screen.queryByRole( 'tooltip', { name: /tooltip text/i } ) - ).not.toBeInTheDocument(); + expect( anchor ).toBeVisible(); - await cleanupTooltip( user ); - } ); + // Hover over the anchor, tooltip should show + await hover( anchor ); + await waitExpectTooltipToShow(); - it( 'should respect custom delay prop when showing tooltip', async () => { - const user = userEvent.setup(); - const ADDITIONAL_DELAY = 100; + // Click the anchor, tooltip should hide + await click( anchor ); + await waitExpectTooltipToHide(); + } ); - render( - - ); + it( 'should not hide tooltip when the tooltip anchor is clicked and the `hideOnClick` prop is `false', async () => { + render( + <> + + + + ); - await user.hover( screen.getByRole( 'button', { name: /Button/i } ) ); + const anchor = screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ); - // Advance time by default delay - await new Promise( ( resolve ) => - setTimeout( resolve, TOOLTIP_DELAY ) - ); + expect( anchor ).toBeVisible(); - // Tooltip hasn't appeared yet - expect( - screen.queryByRole( 'tooltip', { name: /tooltip text/i } ) - ).not.toBeInTheDocument(); + // Hover over the anchor, tooltip should show + await hover( anchor ); + await waitExpectTooltipToShow(); - // wait for additional delay for tooltip to appear - await waitFor( - () => - new Promise( ( resolve ) => - setTimeout( resolve, ADDITIONAL_DELAY ) - ) - ); + // Click the anchor, tooltip should not hide + await click( anchor ); + await waitExpectTooltipToShow(); - expect( - screen.getByRole( 'tooltip', { name: /tooltip text/i } ) - ).toBeVisible(); - - await cleanupTooltip( user ); + // Click another button, tooltip should hide + await click( screen.getByRole( 'button', { name: 'Click me' } ) ); + await waitExpectTooltipToHide(); + } ); } ); - it( 'should show tooltip when an element is disabled', async () => { - const user = userEvent.setup(); + describe( 'delay', () => { + it( 'should respect custom delay prop when showing tooltip', async () => { + const ADDITIONAL_DELAY = 100; - render( - - - - ); + render( + + ); - const button = screen.getByRole( 'button', { name: /Button/i } ); + const anchor = screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ); + expect( anchor ).toBeVisible(); - expect( button ).toBeVisible(); - expect( button ).toHaveAttribute( 'aria-disabled' ); + // Hover over the anchor + await hover( anchor ); + expectTooltipToBeHidden(); - await user.hover( button ); + // Advance time by default delay + await sleep( TOOLTIP_DELAY ); - expect( - await screen.findByRole( 'tooltip', { name: /tooltip text/i } ) - ).toBeVisible(); + // Tooltip hasn't appeared yet + expectTooltipToBeHidden(); - await cleanupTooltip( user ); - } ); + // Wait for additional delay for tooltip to appear + await sleep( ADDITIONAL_DELAY ); + await waitExpectTooltipToShow(); - it( 'should not show tooltip if the mouse leaves the tooltip anchor before set delay', async () => { - const user = userEvent.setup(); - const onMouseEnterMock = jest.fn(); - const onMouseLeaveMock = jest.fn(); - const MOUSE_LEAVE_DELAY = TOOLTIP_DELAY - 200; + // Hover outside of the anchor, tooltip should hide + await hoverOutside(); + await waitExpectTooltipToHide(); + } ); - render( - <> + it( 'should not show tooltip if the mouse leaves the tooltip anchor before set delay', async () => { + const onMouseEnterMock = jest.fn(); + const onMouseLeaveMock = jest.fn(); + const HOVER_OUTSIDE_ANTICIPATION = 200; + + render( - - - ); - - await user.hover( - screen.getByRole( 'button', { - name: 'Button 1', - } ) - ); - - // Tooltip hasn't appeared yet - expect( - screen.queryByRole( 'tooltip', { name: /tooltip text/i } ) - ).not.toBeInTheDocument(); - expect( onMouseEnterMock ).toHaveBeenCalledTimes( 1 ); - - // Advance time by MOUSE_LEAVE_DELAY time - await new Promise( ( resolve ) => - setTimeout( resolve, MOUSE_LEAVE_DELAY ) - ); - - expect( - screen.queryByRole( 'tooltip', { name: /tooltip text/i } ) - ).not.toBeInTheDocument(); - - // Hover the other button, meaning that the mouse will leave the tooltip anchor - await user.hover( - screen.getByRole( 'button', { - name: 'Button 2', - } ) - ); - - // Tooltip still hasn't appeared yet - expect( - screen.queryByRole( 'tooltip', { name: /tooltip text/i } ) - ).not.toBeInTheDocument(); - expect( onMouseEnterMock ).toHaveBeenCalledTimes( 1 ); - expect( onMouseLeaveMock ).toHaveBeenCalledTimes( 1 ); - - // Advance time again, so that we reach the full TOOLTIP_DELAY time - await new Promise( ( resolve ) => - setTimeout( resolve, TOOLTIP_DELAY ) - ); - - // Tooltip won't show, since the mouse has left the tooltip anchor - expect( - screen.queryByRole( 'tooltip', { name: /tooltip text/i } ) - ).not.toBeInTheDocument(); - - await cleanupTooltip( user ); - } ); - - it( 'should render the shortcut display text when a string is passed as the shortcut', async () => { - const user = userEvent.setup(); + ); - render( ); + const anchor = screen.getByRole( 'button', { + name: 'Tooltip anchor', + } ); + expect( anchor ).toBeVisible(); - await user.hover( screen.getByRole( 'button', { name: /Button/i } ) ); - - await waitFor( () => - expect( screen.getByText( 'shortcut text' ) ).toBeVisible() - ); - - await cleanupTooltip( user ); - } ); + // Hover over the anchor, tooltip hasn't appeared yet + await hover( anchor ); + expect( onMouseEnterMock ).toHaveBeenCalledTimes( 1 ); + expectTooltipToBeHidden(); - it( 'should render the keyboard shortcut display text and aria-label when an object is passed as the shortcut', async () => { - const user = userEvent.setup(); + // Advance time, tooltip hasn't appeared yet because TOOLTIP_DELAY time + // hasn't passed yet + await sleep( TOOLTIP_DELAY - HOVER_OUTSIDE_ANTICIPATION ); + expectTooltipToBeHidden(); - render( - - ); + // Hover outside of the anchor, tooltip still hasn't appeared yet + await hoverOutside(); + expectTooltipToBeHidden(); - await user.hover( screen.getByRole( 'button', { name: /Button/i } ) ); + expect( onMouseEnterMock ).toHaveBeenCalledTimes( 1 ); + expect( onMouseLeaveMock ).toHaveBeenCalledTimes( 1 ); - await waitFor( () => - expect( screen.getByText( '⇧⌘,' ) ).toBeVisible() - ); + // Advance time again, so that we reach the full TOOLTIP_DELAY time + await sleep( HOVER_OUTSIDE_ANTICIPATION ); - expect( screen.getByText( '⇧⌘,' ) ).toHaveAttribute( - 'aria-label', - 'Control + Shift + Comma' - ); - - await cleanupTooltip( user ); + // Tooltip won't show, since the mouse has left the tooltip anchor + expectTooltipToBeHidden(); + } ); } ); - it( 'esc should close modal even when tooltip is visible', async () => { - const user = userEvent.setup(); - const onRequestClose = jest.fn(); - render( - -

Modal content

-
- ); - - expect( - screen.queryByRole( 'tooltip', { name: /close/i } ) - ).not.toBeInTheDocument(); - - await user.hover( - screen.getByRole( 'button', { - name: /Close/i, - } ) - ); - - await waitFor( () => + describe( 'shortcut', () => { + it( 'should show the shortcut in the tooltip when a string is passed as the shortcut', async () => { + render( ); + + // Hover over the anchor, tooltip should show + await hover( + screen.getByRole( 'button', { name: 'Tooltip anchor' } ) + ); + await waitFor( () => + expect( + screen.getByRole( 'tooltip', { + name: 'tooltip text shortcut text', + } ) + ).toBeVisible() + ); + + // Hover outside of the anchor, tooltip should hide + await hoverOutside(); + await waitExpectTooltipToHide(); + } ); + + it( 'should show the shortcut in the tooltip when an object is passed as the shortcut', async () => { + render( + + ); + + // Hover over the anchor, tooltip should show + await hover( + screen.getByRole( 'button', { name: 'Tooltip anchor' } ) + ); + await waitFor( () => + expect( + screen.getByRole( 'tooltip', { + name: 'tooltip text Control + Shift + Comma', + } ) + ).toBeVisible() + ); expect( - screen.getByRole( 'tooltip', { name: /close/i } ) - ).toBeVisible() - ); - - await user.keyboard( '[Escape]' ); - - expect( onRequestClose ).toHaveBeenCalled(); - - await cleanupTooltip( user ); - } ); - - it( 'should associate the tooltip text with its anchor via the accessible description when visible', async () => { - const user = userEvent.setup(); - - render( ); - - await user.hover( - screen.getByRole( 'button', { - name: /Button/i, - } ) - ); - - expect( - await screen.findByRole( 'button', { description: 'tooltip text' } ) - ).toBeInTheDocument(); + screen.getByRole( 'tooltip', { + name: 'tooltip text Control + Shift + Comma', + } ) + ).toHaveTextContent( /⇧⌘,/i ); + + // Hover outside of the anchor, tooltip should hide + await hoverOutside(); + await waitExpectTooltipToHide(); + } ); } ); - it( 'should not hide tooltip when the anchor is clicked if hideOnClick is false', async () => { - const user = userEvent.setup(); - - render( ); - - const button = screen.getByRole( 'button', { name: /Button/i } ); - - await user.hover( button ); - - expect( - await screen.findByRole( 'tooltip', { name: /tooltip text/i } ) - ).toBeVisible(); - - await user.click( button ); - - expect( - screen.getByRole( 'tooltip', { name: /tooltip text/i } ) - ).toBeVisible(); - - await cleanupTooltip( user ); + describe( 'event propagation', () => { + it( 'should close the parent dialog component when pressing the Escape key while the tooltip is visible', async () => { + const onRequestClose = jest.fn(); + render( + +

Modal content

+
+ ); + + expectTooltipToBeHidden(); + + const closeButton = screen.getByRole( 'button', { + name: /close/i, + } ); + + // Hover over the anchor, tooltip should show + await hover( closeButton ); + await waitFor( () => + expect( + screen.getByRole( 'tooltip', { name: /close/i } ) + ).toBeVisible() + ); + + // Press the Escape key, Modal should request to be closed + await press.Escape(); + expect( onRequestClose ).toHaveBeenCalled(); + + // Hover outside of the anchor, tooltip should hide + await hoverOutside(); + await waitExpectTooltipToHide(); + } ); } ); } ); diff --git a/packages/components/src/unit-control/index.tsx b/packages/components/src/unit-control/index.tsx index cc657f5fd67f40..2b8b0528637e84 100644 --- a/packages/components/src/unit-control/index.tsx +++ b/packages/components/src/unit-control/index.tsx @@ -188,7 +188,7 @@ function UnforwardedUnitControl( isUnitSelectTabbable={ isUnitSelectTabbable } onChange={ handleOnUnitChange } size={ - size === 'small' || + [ 'small', 'compact' ].includes( size ) || ( size === 'default' && ! props.__next40pxDefaultSize ) ? 'small' : 'default' diff --git a/packages/compose/CHANGELOG.md b/packages/compose/CHANGELOG.md index a67351e6d5ea87..d6938f4f26f63f 100644 --- a/packages/compose/CHANGELOG.md +++ b/packages/compose/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 6.26.0 (2023-12-27) - ## 6.25.0 (2023-12-13) ## 6.24.0 (2023-11-29) diff --git a/packages/compose/package.json b/packages/compose/package.json index 713a7ac85f8cb3..0dec375ed22430 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/compose", - "version": "6.26.0-prerelease", + "version": "6.25.0", "description": "WordPress higher-order components (HOCs).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-commands/CHANGELOG.md b/packages/core-commands/CHANGELOG.md index 6723b7ace7ba34..1abb8f289e7d9e 100644 --- a/packages/core-commands/CHANGELOG.md +++ b/packages/core-commands/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 0.18.0 (2023-12-27) - ## 0.17.0 (2023-12-13) ## 0.16.0 (2023-11-29) diff --git a/packages/core-commands/package.json b/packages/core-commands/package.json index 61d8e3037961a7..a3e42ee2c236ad 100644 --- a/packages/core-commands/package.json +++ b/packages/core-commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-commands", - "version": "0.18.0-prerelease", + "version": "0.17.0", "description": "WordPress core reusable commands.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index 2e56f84b3633c8..ae8d7543f8fbd5 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 6.26.0 (2023-12-27) - ## 6.25.0 (2023-12-13) ## 6.24.0 (2023-11-29) diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 565ef8e2fdf55c..dfbdc2a073765a 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "6.26.0-prerelease", + "version": "6.25.0", "description": "Access to and manipulation of core WordPress entities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-data/src/footnotes/index.js b/packages/core-data/src/footnotes/index.js index 9458290f9cb40b..b80ba65d142edb 100644 --- a/packages/core-data/src/footnotes/index.js +++ b/packages/core-data/src/footnotes/index.js @@ -75,6 +75,14 @@ export function updateFootnotesFromMeta( blocks, meta ) { html: replacement.innerHTML, } ); countValue.text = String( index + 1 ); + countValue.formats = Array.from( + { length: countValue.text.length }, + () => countValue.formats[ 0 ] + ); + countValue.replacements = Array.from( + { length: countValue.text.length }, + () => countValue.replacements[ 0 ] + ); replacement.innerHTML = toHTMLString( { value: countValue, } ); diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index 5b42b6731716ff..2a9290ac3daa30 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -44,10 +44,10 @@ const entityResolvers = rootEntitiesConfig.reduce( ( result, entity ) => { const entityActions = rootEntitiesConfig.reduce( ( result, entity ) => { const { kind, name } = entity; - result[ getMethodName( kind, name, 'save' ) ] = ( key ) => - actions.saveEntityRecord( kind, name, key ); - result[ getMethodName( kind, name, 'delete' ) ] = ( key, query ) => - actions.deleteEntityRecord( kind, name, key, query ); + result[ getMethodName( kind, name, 'save' ) ] = ( record, options ) => + actions.saveEntityRecord( kind, name, record, options ); + result[ getMethodName( kind, name, 'delete' ) ] = ( key, query, options ) => + actions.deleteEntityRecord( kind, name, key, query, options ); return result; }, {} ); diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index f086fdeff1e6ce..159c65e9ada19c 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,22 +2,24 @@ ## Unreleased -## 1.12.0 (2023-12-27) +### Enhancement + +- Update the template to use `viewModule` in block.json ([#57712](https://github.com/WordPress/gutenberg/pull/57712)). ## 1.11.0 (2023-12-13) -- Add all files to the generated plugin zip. [#56943](https://github.com/WordPress/gutenberg/pull/56943) -- Prevent crash when Gutenberg plugin is not installed. [#56941](https://github.com/WordPress/gutenberg/pull/56941) +- Add all files to the generated plugin zip ([#56943](https://github.com/WordPress/gutenberg/pull/56943)). +- Prevent crash when Gutenberg plugin is not installed ([#56941](https://github.com/WordPress/gutenberg/pull/56941)). ## 1.10.1 (2023-12-07) -- Update template to use modules instead of scripts. [#56694](https://github.com/WordPress/gutenberg/pull/56694) +- Update template to use modules instead of scripts ([#56694](https://github.com/WordPress/gutenberg/pull/56694)). ## 1.10.0 (2023-11-29) ### Enhancement -- Update `view.js` and `render.php` templates to the new `store()` API. [#56613](https://github.com/WordPress/gutenberg/pull/56613) +- Update `view.js` and `render.php` templates to the new `store()` API ([#56613](https://github.com/WordPress/gutenberg/pull/56613)). ## 1.9.0 (2023-11-16) @@ -37,4 +39,4 @@ ### Enhancement -- Moves the `example` property into block.json by leveraging changes to create-block to now support `example`. [#52801](https://github.com/WordPress/gutenberg/pull/52801) +- Moves the `example` property into block.json by leveraging changes to create-block to now support `example` ([#52801](https://github.com/WordPress/gutenberg/pull/52801)). diff --git a/packages/create-block-interactive-template/README.md b/packages/create-block-interactive-template/README.md index cc0530c0630549..adf3cab6594cc9 100644 --- a/packages/create-block-interactive-template/README.md +++ b/packages/create-block-interactive-template/README.md @@ -10,6 +10,8 @@ This block template can be used by running the following command: npx @wordpress/create-block --template @wordpress/create-block-interactive-template ``` +It requires Gutenberg 17.5 or higher. + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. diff --git a/packages/create-block-interactive-template/block-templates/render.php.mustache b/packages/create-block-interactive-template/block-templates/render.php.mustache index 0f6883a9362407..960da619f790a4 100644 --- a/packages/create-block-interactive-template/block-templates/render.php.mustache +++ b/packages/create-block-interactive-template/block-templates/render.php.mustache @@ -13,11 +13,6 @@ // Generate unique id for aria-controls. $unique_id = wp_unique_id( 'p-' ); - -// Enqueue the view file. -if (function_exists('gutenberg_enqueue_module')) { - gutenberg_enqueue_module( '{{namespace}}-view' ); -} ?>
!! value ) diff --git a/packages/create-block/lib/scaffold.js b/packages/create-block/lib/scaffold.js index 49d3cbf794777a..bd9ba0396b75e3 100644 --- a/packages/create-block/lib/scaffold.js +++ b/packages/create-block/lib/scaffold.js @@ -44,6 +44,7 @@ module.exports = async ( editorStyle, style, render, + viewModule, viewScript, variantVars, customPackageJSON, @@ -84,6 +85,7 @@ module.exports = async ( editorStyle, style, render, + viewModule, viewScript, variantVars, customPackageJSON, diff --git a/packages/create-block/package.json b/packages/create-block/package.json index 9a628ffdf4a331..32a04f4b9857e5 100644 --- a/packages/create-block/package.json +++ b/packages/create-block/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block", - "version": "4.33.0-prerelease", + "version": "4.32.0", "description": "Generates PHP, JS and CSS code for registering a block for a WordPress plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/customize-widgets/CHANGELOG.md b/packages/customize-widgets/CHANGELOG.md index 4995c6088924fe..733a2e88914e89 100644 --- a/packages/customize-widgets/CHANGELOG.md +++ b/packages/customize-widgets/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.26.0 (2023-12-27) - ## 4.25.0 (2023-12-13) ## 4.24.0 (2023-11-29) diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 64b1eabcaeee4a..88b32f5a23bec8 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/customize-widgets", - "version": "4.26.0-prerelease", + "version": "4.25.0", "description": "Widgets blocks in Customizer Module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data-controls/CHANGELOG.md b/packages/data-controls/CHANGELOG.md index c6e8c689b8c071..a5be93faab0043 100644 --- a/packages/data-controls/CHANGELOG.md +++ b/packages/data-controls/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.18.0 (2023-12-27) - ## 3.17.0 (2023-12-13) ## 3.16.0 (2023-11-29) diff --git a/packages/data-controls/package.json b/packages/data-controls/package.json index 0f26fca0fc6539..5018027820afe2 100644 --- a/packages/data-controls/package.json +++ b/packages/data-controls/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data-controls", - "version": "3.18.0-prerelease", + "version": "3.17.0", "description": "A set of common controls for the @wordpress/data api.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 90c026268e971e..328928d93d509b 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 9.19.0 (2023-12-27) - ## 9.18.0 (2023-12-13) ## 9.17.0 (2023-11-29) diff --git a/packages/data/package.json b/packages/data/package.json index cb57d35b87f138..37e9d8753cda02 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "9.19.0-prerelease", + "version": "9.18.0", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 34def6573af19f..bd4afefab43fef 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,6 +2,4 @@ ## Unreleased -## 0.3.0 (2023-12-27) - ## 0.2.0 (2023-12-13) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 558c509139a666..86e4e3e38c3dfc 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -236,7 +236,8 @@ Array of operations that can be performed upon each record. Each action is an ob - `isLoading`: whether the data is loading. `false` by default. - `supportedLayouts`: array of layouts supported. By default, all are: `table`, `grid`, `list`. - `deferredRendering`: whether the items should be rendered asynchronously. Useful when there's a field that takes a lot of time (e.g.: previews). `false` by default. -- `onSelectionChange`: callback that returns the selected items. So far, only the `list` view implements this. +- `onSelectionChange`: callback that signals the user selected one of more items, and takes them as parameter. So far, only the `list` view implements it. +- `onDetailsChange`: callback that signals the user triggered the details for one of more items, and takes them as paremeter. So far, only the `list` view implements it. ## Contributing to this package diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 4136be59c330da..4dfb35948af468 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dataviews", - "version": "0.3.0-prerelease", + "version": "0.2.0", "description": "DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index a9d8d78509dc46..4f77a35c52d2ea 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -23,6 +23,7 @@ const { DropdownMenuRadioItemV2: DropdownMenuRadioItem, DropdownMenuSeparatorV2: DropdownMenuSeparator, DropdownMenuItemLabelV2: DropdownMenuItemLabel, + DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText, } = unlock( componentsPrivateApis ); function WithSeparators( { children } ) { @@ -117,10 +118,10 @@ export default function AddFilter( { filters, view, onChangeView } ) { return ( { + onChange={ ( e ) => { onChangeView( { ...view, page: 1, @@ -132,7 +133,9 @@ export default function AddFilter( { filters, view, onChangeView } ) { activeOperator, value: isActive ? undefined - : element.value, + : e + .target + .value, }, ], } ); @@ -141,6 +144,13 @@ export default function AddFilter( { filters, view, onChangeView } ) { { element.label } + { !! element.description && ( + + { + element.description + } + + ) } ); } ) } @@ -172,7 +182,7 @@ export default function AddFilter( { filters, view, onChangeView } ) { ] ) => ( item.id; +const defaultOnSelectionChange = () => {}; export default function DataViews( { view, @@ -30,15 +31,19 @@ export default function DataViews( { isLoading = false, paginationInfo, supportedLayouts, - onSelectionChange, + onSelectionChange = defaultOnSelectionChange, + onDetailsChange = null, deferredRendering = false, } ) { const [ selection, setSelection ] = useState( [] ); - const onSetSelection = ( items ) => { - setSelection( items.map( ( item ) => item.id ) ); - onSelectionChange( items ); - }; + const onSetSelection = useCallback( + ( items ) => { + setSelection( items.map( ( item ) => item.id ) ); + onSelectionChange( items ); + }, + [ setSelection, onSelectionChange ] + ); const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type @@ -54,7 +59,7 @@ export default function DataViews( { { search && ( @@ -65,13 +70,13 @@ export default function DataViews( { /> ) } diff --git a/packages/dataviews/src/dropdown-menu-helper.js b/packages/dataviews/src/dropdown-menu-helper.js index 2fd6ae18a4536e..ce0ace8f61e551 100644 --- a/packages/dataviews/src/dropdown-menu-helper.js +++ b/packages/dataviews/src/dropdown-menu-helper.js @@ -28,7 +28,10 @@ const radioCheck = ( * component, which allows deselecting selected values. */ export const DropdownMenuRadioItemCustom = forwardRef( - ( { checked, name, value, onChange, onClick, ...props }, ref ) => { + function DropdownMenuRadioItemCustom( + { checked, name, value, hideOnClick, onChange, onClick, ...props }, + ref + ) { const onClickHandler = ( e ) => { onClick?.( e ); onChange?.( { ...e, target: { ...e.target, value } } ); @@ -39,13 +42,13 @@ export const DropdownMenuRadioItemCustom = forwardRef( role="menuitemradio" name={ name } aria-checked={ checked } - hideOnClick={ false } + hideOnClick={ !! hideOnClick } prefix={ checked ? ( ) : ( ) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index e255561660648c..723fee5b3ee52e 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -23,6 +23,7 @@ const { DropdownMenuItemV2: DropdownMenuItem, DropdownMenuSeparatorV2: DropdownMenuSeparator, DropdownMenuItemLabelV2: DropdownMenuItemLabel, + DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText, } = unlock( componentsPrivateApis ); const FilterText = ( { activeElement, filterInView, filter } ) => { @@ -103,7 +104,7 @@ export default function FilterSummary( { filter, view, onChangeView } ) { return ( @@ -126,6 +127,11 @@ export default function FilterSummary( { filter, view, onChangeView } ) { { element.label } + { !! element.description && ( + + { element.description } + + ) } ); } ) } @@ -150,7 +156,7 @@ export default function FilterSummary( { filter, view, onChangeView } ) { ( [ operator, { label, key } ] ) => ( { diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index 0ba9d1d50a969b..a101a077539900 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { memo } from '@wordpress/element'; + /** * Internal dependencies */ @@ -21,7 +26,7 @@ const sanitizeOperators = ( field ) => { ); }; -export default function Filters( { fields, view, onChangeView } ) { +const Filters = memo( function Filters( { fields, view, onChangeView } ) { const filters = []; fields.forEach( ( field ) => { if ( ! field.type ) { @@ -35,10 +40,13 @@ export default function Filters( { fields, view, onChangeView } ) { switch ( field.type ) { case ENUMERATION_TYPE: + if ( ! field.elements?.length ) { + return; + } filters.push( { field: field.id, name: field.header, - elements: field.elements || [], + elements: field.elements, operators, isVisible: view.filters.some( ( f ) => @@ -88,4 +96,6 @@ export default function Filters( { fields, view, onChangeView } ) { } return filterComponents; -} +} ); + +export default Filters; diff --git a/packages/dataviews/src/index.js b/packages/dataviews/src/index.js index 01c67b34c5c99d..7b0a5d35abf92d 100644 --- a/packages/dataviews/src/index.js +++ b/packages/dataviews/src/index.js @@ -1,2 +1,3 @@ export { default as DataViews } from './dataviews'; +export { sortByTextFields, getPaginationResults } from './utils'; export { VIEW_LAYOUTS } from './constants'; diff --git a/packages/dataviews/src/item-actions.js b/packages/dataviews/src/item-actions.js index 473615214da7b2..974c5c5d0d5c3f 100644 --- a/packages/dataviews/src/item-actions.js +++ b/packages/dataviews/src/item-actions.js @@ -21,6 +21,7 @@ const { DropdownMenuGroupV2: DropdownMenuGroup, DropdownMenuItemV2: DropdownMenuItem, DropdownMenuItemLabelV2: DropdownMenuItemLabel, + kebabCase, } = unlock( componentsPrivateApis ); function ButtonTrigger( { action, onClick } ) { @@ -58,12 +59,17 @@ function ActionWithModal( { action, item, ActionTrigger } ) { { isModalOpen && ( { setIsModalOpen( false ); } } - overlayClassName="dataviews-action-modal" + overlayClassName={ `dataviews-action-modal dataviews-action-modal__${ kebabCase( + action.id + ) }` } > @@ -91,6 +91,6 @@ function Pagination( { ) ); -} +} ); export default Pagination; diff --git a/packages/dataviews/src/search.js b/packages/dataviews/src/search.js index 10a578b49aab24..03bcf0511892b1 100644 --- a/packages/dataviews/src/search.js +++ b/packages/dataviews/src/search.js @@ -2,11 +2,11 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect, useRef, memo } from '@wordpress/element'; import { SearchControl } from '@wordpress/components'; import { useDebouncedInput } from '@wordpress/compose'; -export default function Search( { label, view, onChangeView } ) { +const Search = memo( function Search( { label, view, onChangeView } ) { const [ search, setSearch, debouncedSearch ] = useDebouncedInput( view.search ); @@ -35,4 +35,6 @@ export default function Search( { label, view, onChangeView } ) { size="compact" /> ); -} +} ); + +export default Search; diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 3076162dc8de80..80630050b68efb 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -10,7 +10,7 @@ } } -.dataviews__filters-view-actions { +.dataviews-filters__view-actions { padding: $grid-unit-15 $grid-unit-40; .components-search-control { flex-grow: 1; @@ -55,7 +55,7 @@ margin: $grid-unit-40 0 $grid-unit-20; } -.dataviews-table-view { +.dataviews-view-table { width: 100%; text-indent: 0; border-color: inherit; @@ -85,9 +85,18 @@ tr { border-bottom: 1px solid $gray-100; + .dataviews-view-table-header-button { + gap: $grid-unit-05; + } + td:first-child, th:first-child { padding-left: $grid-unit-40; + + .dataviews-view-table-header-button, + .dataviews-view-table-header { + margin-left: - #{$grid-unit-10}; + } } td:last-child, @@ -112,17 +121,27 @@ th { position: sticky; top: -1px; - background-color: lighten($gray-100, 4%); + background-color: $white; box-shadow: inset 0 -#{$border-width} 0 $gray-100; - border-top: 1px solid $gray-100; - padding-top: $grid-unit-05; - padding-bottom: $grid-unit-05; + padding-top: $grid-unit-10; + padding-bottom: $grid-unit-10; + z-index: 1; + font-size: 11px; + text-transform: uppercase; + font-weight: 500; + padding-left: $grid-unit-05; } } - .dataviews-table-header-button { - padding: 0; - gap: $grid-unit-05; + .dataviews-view-table-header-button { + padding: $grid-unit-05 $grid-unit-10; + font-size: 11px; + text-transform: uppercase; + font-weight: 500; + + &:not(:hover) { + color: $gray-900; + } span { speak: none; @@ -132,9 +151,13 @@ } } } + + .dataviews-view-table-header { + padding-left: $grid-unit-05; + } } -.dataviews-grid-view { +.dataviews-view-grid { margin-bottom: $grid-unit-30; grid-template-columns: repeat(2, minmax(0, 1fr)) !important; padding: 0 $grid-unit-40; @@ -148,10 +171,22 @@ } .dataviews-view-grid__card { - h3 { // Todo: A better way to target this - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + .dataviews-view-grid__primary-field { + .dataviews-view-grid__title-field { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + font-size: $default-font-size; + width: 100%; + } + + .dataviews-view-grid__title-field a, + button.dataviews-view-grid__title-field { + font-weight: 500; + color: $gray-900; + text-decoration: none; + } } } @@ -172,12 +207,6 @@ .dataviews-view-grid__primary-field { min-height: $grid-unit-30; - - a { - color: $gray-900; - text-decoration: none; - font-weight: 500; - } } .dataviews-view-grid__fields { @@ -196,29 +225,71 @@ } } -.dataviews-list-view { +.dataviews-view-list { margin: 0; + padding: $grid-unit-10; li { - border-bottom: $border-width solid $gray-100; margin: 0; - &:first-child { - border-top: $border-width solid $gray-100; + + .dataviews-view-list__item-wrapper { + position: relative; + padding-right: $grid-unit-30; + border-radius: $grid-unit-05; + + &::after { + position: absolute; + content: ""; + top: 100%; + left: $grid-unit-30; + right: $grid-unit-30; + background: $gray-100; + height: 1px; + } } - &:last-child { - border-bottom: 0; + + &:not(.is-selected):hover { + color: var(--wp-admin-theme-color); + + .dataviews-view-list__fields { + color: var(--wp-admin-theme-color); + } } } - .dataviews-list-view__item { - padding: $grid-unit-15 $grid-unit-40; - cursor: default; - &:focus, - &:hover { - background-color: lighten($gray-100, 3%); + li.is-selected, + li.is-selected:focus-within { + .dataviews-view-list__item-wrapper { + background-color: var(--wp-admin-theme-color); + color: $white; + + .dataviews-view-list__fields, + .components-button { + color: $white; + } + + &::after { + background: transparent; + } } + } + + .dataviews-view-list__item { + padding: $grid-unit-15 0 $grid-unit-15 $grid-unit-30; + width: 100%; + cursor: pointer; &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + &::before { + position: absolute; + content: ""; + top: -1px; + right: -1px; + bottom: -1px; + left: -1px; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + z-index: -1; + border-radius: $grid-unit-05; + } } h3 { overflow: hidden; @@ -227,22 +298,12 @@ } } - .dataviews-list-view__item-selected, - .dataviews-list-view__item-selected:hover { - background-color: $gray-100; - - &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - } - } - - .dataviews-list-view__media-wrapper { + .dataviews-view-list__media-wrapper { min-width: $grid-unit-40; height: $grid-unit-40; border-radius: $grid-unit-05; overflow: hidden; position: relative; - margin-top: $grid-unit-05; &::after { content: ""; @@ -256,19 +317,19 @@ } } - .dataviews-list-view__media-placeholder { + .dataviews-view-list__media-placeholder { min-width: $grid-unit-40; height: $grid-unit-40; background-color: $gray-200; } - .dataviews-list-view__fields { + .dataviews-view-list__fields { color: $gray-700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - .dataviews-list-view__field { + .dataviews-view-list__field { margin-right: $grid-unit-15; &:last-child { @@ -276,6 +337,31 @@ } } } + + & + .dataviews-pagination { + justify-content: space-between; + } + + .dataviews-view-list__details-button { + align-self: center; + opacity: 0; + } + + li.is-selected, + li:hover, + li:focus-within { + .dataviews-view-list__details-button { + opacity: 1; + } + } + + li.is-selected { + .dataviews-view-list__details-button { + &:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) currentColor; + } + } + } } .dataviews-action-modal { @@ -287,7 +373,7 @@ padding: 0 $grid-unit-40; } -.dataviews__filters-custom-menu-radio-item-prefix { +.dataviews-filters__custom-menu-radio-item-prefix { display: block; width: 24px; } diff --git a/packages/dataviews/src/utils.js b/packages/dataviews/src/utils.js new file mode 100644 index 00000000000000..2aa454b272c80e --- /dev/null +++ b/packages/dataviews/src/utils.js @@ -0,0 +1,51 @@ +/** + * Helper util to sort data by text fields, when sorting is done client side. + * + * @param {Object} params Function params. + * @param {Object[]} params.data Data to sort. + * @param {Object} params.view Current view object. + * @param {Object[]} params.fields Array of available fields. + * @param {string[]} params.textFields Array of the field ids to sort. + * + * @return {Object[]} Sorted data. + */ +export const sortByTextFields = ( { data, view, fields, textFields } ) => { + const sortedData = [ ...data ]; + const fieldId = view.sort.field; + if ( textFields.includes( fieldId ) ) { + const fieldToSort = fields.find( ( field ) => { + return field.id === fieldId; + } ); + sortedData.sort( ( a, b ) => { + const valueA = fieldToSort.getValue( { item: a } ) ?? ''; + const valueB = fieldToSort.getValue( { item: b } ) ?? ''; + return view.sort.direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + } ); + } + return sortedData; +}; + +/** + * Helper util to get the paginated data and the paginateInfo needed, + * when pagination is done client side. + * + * @param {Object} params Function params. + * @param {Object[]} params.data Available data. + * @param {Object} params.view Current view object. + * + * @return {Object} Paginated data and paginationInfo. + */ +export function getPaginationResults( { data, view } ) { + const start = ( view.page - 1 ) * view.perPage; + const totalItems = data?.length || 0; + data = data?.slice( start, start + view.perPage ); + return { + data, + paginationInfo: { + totalItems, + totalPages: Math.ceil( totalItems / view.perPage ), + }, + }; +} diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index 458032d68ac70c..65c4dff8c36f71 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -6,18 +6,19 @@ import { privateApis as componentsPrivateApis, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { memo } from '@wordpress/element'; /** * Internal dependencies */ import { unlock } from './lock-unlock'; import { VIEW_LAYOUTS, LAYOUT_TABLE, SORTING_DIRECTIONS } from './constants'; -import { DropdownMenuRadioItemCustom } from './dropdown-menu-helper'; const { DropdownMenuV2: DropdownMenu, DropdownMenuGroupV2: DropdownMenuGroup, DropdownMenuItemV2: DropdownMenuItem, + DropdownMenuRadioItemV2: DropdownMenuRadioItem, DropdownMenuCheckboxItemV2: DropdownMenuCheckboxItem, DropdownMenuItemLabelV2: DropdownMenuItemLabel, } = unlock( componentsPrivateApis ); @@ -49,11 +50,12 @@ function ViewTypeMenu( { view, onChangeView, supportedLayouts } ) { > { _availableViews.map( ( availableView ) => { return ( - { onChangeView( { ...view, @@ -64,7 +66,7 @@ function ViewTypeMenu( { view, onChangeView, supportedLayouts } ) { { availableView.label } - + ); } ) } @@ -88,21 +90,23 @@ function PageSizeMenu( { view, onChangeView } ) { > { PAGE_SIZE_VALUES.map( ( size ) => { return ( - { + onChange={ () => { onChangeView( { ...view, - perPage: e.target.value, + // `e.target.value` holds the same value as `size` but as a string, + // so we use `size` directly to avoid parsing to int. + perPage: size, page: 1, } ); } } > { size } - + ); } ) } @@ -208,18 +212,25 @@ function SortMenu( { fields, view, onChangeView } ) { sortedDirection === direction && field.id === currentSortedField.id; + const value = `${ field.id }-${ direction }`; + return ( - { + onChange={ () => { onChangeView( { ...view, sort: { field: field.id, - direction: e.target.value, + direction, }, } ); } } @@ -227,7 +238,7 @@ function SortMenu( { fields, view, onChangeView } ) { { info.label } - + ); } ) } @@ -238,7 +249,7 @@ function SortMenu( { fields, view, onChangeView } ) { ); } -export default function ViewActions( { +const ViewActions = memo( function ViewActions( { fields, view, onChangeView, @@ -281,4 +292,6 @@ export default function ViewActions( { ); -} +} ); + +export default ViewActions; diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index e43c9744c560f8..7c18d31dccd193 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -42,7 +42,7 @@ export default function ViewGrid( { gap={ 8 } columns={ 2 } alignment="top" - className="dataviews-grid-view" + className="dataviews-view-grid" > { usedData.map( ( item ) => (
- { field.render( { item } ) } + { renderedValue }
); diff --git a/packages/dataviews/src/view-list.js b/packages/dataviews/src/view-list.js index ab544e0eec9c2c..70755c4aa35c51 100644 --- a/packages/dataviews/src/view-list.js +++ b/packages/dataviews/src/view-list.js @@ -10,8 +10,11 @@ import { useAsyncList } from '@wordpress/compose'; import { __experimentalHStack as HStack, __experimentalVStack as VStack, + Button, } from '@wordpress/components'; import { ENTER, SPACE } from '@wordpress/keycodes'; +import { info } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; export default function ViewList( { view, @@ -19,6 +22,7 @@ export default function ViewList( { data, getItemId, onSelectionChange, + onDetailsChange, selection, deferredRendering, } ) { @@ -46,39 +50,38 @@ export default function ViewList( { }; return ( -
    +
      { usedData.map( ( item ) => { return ( -
    • -
      onSelectionChange( [ item ] ) } - > - -
      - { mediaField?.render( { item } ) || ( -
      - ) } -
      - +
    • + +
      onSelectionChange( [ item ] ) } + > + +
      + { mediaField?.render( { item } ) || ( +
      + ) } +
      { primaryField?.render( { item } ) } -
      +
      { visibleFields.map( ( field ) => { return ( { field.render( { item, @@ -89,8 +92,19 @@ export default function ViewList( {
      - -
      +
      + { onDetailsChange && ( +
    • ); } ) } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 200da945a101a1..dc76572e30494e 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; + /** * WordPress dependencies */ @@ -9,7 +14,15 @@ import { Icon, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { Children, Fragment } from '@wordpress/element'; +import { + Children, + Fragment, + forwardRef, + useEffect, + useId, + useRef, + useState, +} from '@wordpress/element'; /** * Internal dependencies @@ -23,8 +36,10 @@ const { DropdownMenuV2: DropdownMenu, DropdownMenuGroupV2: DropdownMenuGroup, DropdownMenuItemV2: DropdownMenuItem, + DropdownMenuRadioItemV2: DropdownMenuRadioItem, DropdownMenuSeparatorV2: DropdownMenuSeparator, DropdownMenuItemLabelV2: DropdownMenuItemLabel, + DropdownMenuItemHelpTextV2: DropdownMenuItemHelpText, } = unlock( componentsPrivateApis ); const sortArrows = { asc: '↑', desc: '↓' }; @@ -39,7 +54,10 @@ const sanitizeOperators = ( field ) => { ); }; -function HeaderMenu( { field, view, onChangeView } ) { +const HeaderMenu = forwardRef( function HeaderMenu( + { field, view, onChangeView, onHide }, + ref +) { const isHidable = field.enableHiding !== false; const isSortable = field.enableSorting !== false; @@ -72,8 +90,9 @@ function HeaderMenu( { field, view, onChangeView } ) { trigger={
); diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss index acd6dde411d928..a3cf623a2d1aa5 100644 --- a/packages/edit-post/src/components/header/style.scss +++ b/packages/edit-post/src/components/header/style.scss @@ -174,7 +174,7 @@ .components-menu-items-choice .components-menu-items__item-icon.components-menu-items__item-icon { display: block; } - .edit-post-header-toolbar__inserter-toggle.edit-post-header-toolbar__inserter-toggle, + .editor-document-tools__inserter-toggle.editor-document-tools__inserter-toggle, .interface-pinned-items .components-button { padding-left: $grid-unit; padding-right: $grid-unit; @@ -275,8 +275,8 @@ visibility: hidden; } - & > .edit-post-header__toolbar .edit-post-header-toolbar__inserter-toggle, - & > .edit-post-header__toolbar .edit-post-header-toolbar__document-overview-toggle, + & > .edit-post-header__toolbar .editor-document-tools__inserter-toggle, + & > .edit-post-header__toolbar .editor-document-tools__document-overview-toggle, & > .edit-post-header__settings > .editor-preview-dropdown, & > .edit-post-header__settings > .interface-pinned-items { display: none; diff --git a/packages/edit-post/src/components/header/writing-menu/index.js b/packages/edit-post/src/components/header/writing-menu/index.js index 11d07e52ec590d..754e65af5200d5 100644 --- a/packages/edit-post/src/components/header/writing-menu/index.js +++ b/packages/edit-post/src/components/header/writing-menu/index.js @@ -27,7 +27,7 @@ function WritingMenu() { const toggleDistractionFree = () => { registry.batch( () => { - setPreference( 'core/edit-post', 'fixedToolbar', true ); + setPreference( 'core', 'fixedToolbar', true ); setIsInserterOpened( false ); setIsListViewOpened( false ); closeGeneralSidebar(); @@ -35,7 +35,7 @@ function WritingMenu() { }; const turnOffDistractionFree = () => { - setPreference( 'core/edit-post', 'distractionFree', false ); + setPreference( 'core', 'distractionFree', false ); }; const isLargeViewport = useViewportMatch( 'medium' ); @@ -46,7 +46,7 @@ function WritingMenu() { return ( { + const { get } = select( preferencesStore ); const { getEditorSettings, getPostTypeLabel } = select( editorStore ); const editorSettings = getEditorSettings(); const postTypeLabel = getPostTypeLabel(); @@ -188,17 +189,14 @@ function Layout() { nextShortcut: select( keyboardShortcutsStore ).getAllShortcutKeyCombinations( 'core/edit-post/next-region' ), - showIconLabels: - select( editPostStore ).isFeatureActive( 'showIconLabels' ), - isDistractionFree: - select( editPostStore ).isFeatureActive( 'distractionFree' ), - showBlockBreadcrumbs: select( editPostStore ).isFeatureActive( - 'showBlockBreadcrumbs' - ), + showIconLabels: get( 'core', 'showIconLabels' ), + isDistractionFree: get( 'core', 'distractionFree' ), + showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), // translators: Default label for the Document in the Block Breadcrumb. documentLabel: postTypeLabel || _x( 'Document', 'noun' ), hasBlockSelected: !! select( blockEditorStore ).getBlockSelectionStart(), + hasHistory: !! getEditorSettings().goBack, }; }, [] ); @@ -286,7 +284,7 @@ function Layout() { return ( <> - + diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js index a9c38ee1ccb8d5..50472f79249305 100644 --- a/packages/edit-post/src/components/layout/index.native.js +++ b/packages/edit-post/src/components/layout/index.native.js @@ -22,7 +22,11 @@ import { Tooltip, __unstableAutocompletionItemsSlot as AutocompletionItemsSlot, } from '@wordpress/components'; -import { AutosaveMonitor, store as editorStore } from '@wordpress/editor'; +import { + AutosaveMonitor, + OfflineStatus, + store as editorStore, +} from '@wordpress/editor'; import { sendNativeEditorDidLayout } from '@wordpress/react-native-bridge'; /** @@ -148,6 +152,10 @@ class Layout extends Component { onLayout={ this.onRootViewLayout } > + { + // eslint-disable-next-line no-undef + __DEV__ && + } { isHtmlView ? this.renderHTML() : this.renderVisual() } { ! isHtmlView && Platform.OS === 'android' && ( diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index 20fecacbff655c..a422e4d0a207a2 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -44,14 +44,14 @@ export default function EditPostPreferencesModal() { const [ isModalActive, showBlockBreadcrumbsOption ] = useSelect( ( select ) => { const { getEditorSettings } = select( editorStore ); - const { getEditorMode, isFeatureActive } = select( editPostStore ); + const { getEditorMode } = select( editPostStore ); + const { get } = select( preferencesStore ); const modalActive = select( interfaceStore ).isModalActive( PREFERENCES_MODAL_NAME ); const mode = getEditorMode(); const isRichEditingEnabled = getEditorSettings().richEditingEnabled; - const isDistractionFreeEnabled = - isFeatureActive( 'distractionFree' ); + const isDistractionFreeEnabled = get( 'core', 'distractionFree' ); return [ modalActive, ! isDistractionFreeEnabled && @@ -70,14 +70,14 @@ export default function EditPostPreferencesModal() { const { set: setPreference } = useDispatch( preferencesStore ); const toggleDistractionFree = () => { - setPreference( 'core/edit-post', 'fixedToolbar', true ); + setPreference( 'core', 'fixedToolbar', true ); setIsInserterOpened( false ); setIsListViewOpened( false ); closeGeneralSidebar(); }; const turnOffDistractionFree = () => { - setPreference( 'core/edit-post', 'distractionFree', false ); + setPreference( 'core', 'distractionFree', false ); }; const sections = useMemo( @@ -95,12 +95,13 @@ export default function EditPostPreferencesModal() { help={ __( 'Review settings, such as visibility and tags.' ) } - label={ __( 'Enable pre-publish flow' ) } + label={ __( 'Enable pre-publish checks' ) } /> ) } { showBlockBreadcrumbsOption && ( ) } { - const { isFeatureActive } = select( editPostStore ); - return { - isChecked: isFeatureActive( featureName ), - }; - } ), - withDispatch( ( dispatch, { featureName, onToggle = () => {} } ) => ( { - onChange: () => { - onToggle(); - dispatch( editPostStore ).toggleFeature( featureName ); - }, - } ) ) -)( BaseOption ); +export default function EnableFeature( props ) { + const { + scope = 'core/edit-post', + featureName, + onToggle = () => {}, + ...remainingProps + } = props; + const isChecked = useSelect( + ( select ) => !! select( preferencesStore ).get( scope, featureName ), + [ scope, featureName ] + ); + const { toggle } = useDispatch( preferencesStore ); + const onChange = () => { + onToggle(); + toggle( scope, featureName ); + }; + return ( + + ); +} diff --git a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/meta-boxes-section.js.snap b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/meta-boxes-section.js.snap index 2246e0f12fdef7..c39f90d4aa0e5d 100644 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/meta-boxes-section.js.snap +++ b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/meta-boxes-section.js.snap @@ -65,42 +65,46 @@ exports[`MetaBoxesSection renders a Custom Fields option 1`] = `
- - - - - - + + + + + + +
@@ -173,126 +177,130 @@ exports[`MetaBoxesSection renders a Custom Fields option and meta box options 1`
- - - - - - + + + + + + +
-
-
- - - - - - + + + + + + +
-
-
- - - - - - + + + + + + +
@@ -365,85 +373,89 @@ exports[`MetaBoxesSection renders meta box options 1`] = `
- - - - - - + + + + + + +
-
-
- - - - - - + + + + + + +
diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-outline.js b/packages/edit-post/src/components/secondary-sidebar/list-view-outline.js deleted file mode 100644 index 6d27767bdfd0bd..00000000000000 --- a/packages/edit-post/src/components/secondary-sidebar/list-view-outline.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { - DocumentOutline, - WordCount, - TimeToRead, - CharacterCount, -} from '@wordpress/editor'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { - __experimentalText as Text, - Path, - SVG, - Line, - Rect, -} from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -function EmptyOutlineIllustration() { - return ( - - - - - - - - - - - - - - - - ); -} - -export default function ListViewOutline() { - const { headingCount } = useSelect( ( select ) => { - const { getGlobalBlockCount } = select( blockEditorStore ); - return { - headingCount: getGlobalBlockCount( 'core/heading' ), - }; - }, [] ); - return ( - <> -
-
- { __( 'Characters:' ) } - - - -
-
- { __( 'Words:' ) } - -
-
- { __( 'Time to read:' ) } - -
-
- { headingCount > 0 ? ( - - ) : ( -
- -

- { __( - 'Navigate the structure of your document and address issues like empty or incorrect heading levels.' - ) } -

-
- ) } - - ); -} diff --git a/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js b/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js index 39b9e4362ce32c..ac0bf9f52bed5d 100644 --- a/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/plugin-sidebar/index.js @@ -7,11 +7,6 @@ import { __ } from '@wordpress/i18n'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as editorStore } from '@wordpress/editor'; -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - /** * Renders a sidebar when activated. The contents within the `PluginSidebar` will appear as content within the sidebar. * It also automatically renders a corresponding `PluginSidebarMenuItem` component when `isPinnable` flag is set to `true`. @@ -78,14 +73,12 @@ import { store as editPostStore } from '../../../store'; * ``` */ export default function PluginSidebarEditPost( { className, ...props } ) { - const { postTitle, shortcut, showIconLabels } = useSelect( ( select ) => { + const { postTitle, shortcut } = useSelect( ( select ) => { return { postTitle: select( editorStore ).getEditedPostAttribute( 'title' ), shortcut: select( keyboardShortcutsStore ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ), - showIconLabels: - select( editPostStore ).isFeatureActive( 'showIconLabels' ), }; }, [] ); return ( @@ -95,7 +88,6 @@ export default function PluginSidebarEditPost( { className, ...props } ) { smallScreenTitle={ postTitle || __( '(no title)' ) } scope="core/edit-post" toggleShortcut={ shortcut } - showIconLabels={ showIconLabels } { ...props } /> ); diff --git a/packages/edit-post/src/components/text-editor/style.scss b/packages/edit-post/src/components/text-editor/style.scss index c02e983057e6ef..ab248317de1dbf 100644 --- a/packages/edit-post/src/components/text-editor/style.scss +++ b/packages/edit-post/src/components/text-editor/style.scss @@ -39,10 +39,8 @@ margin-right: auto; @include break-large() { - padding: $grid-unit-20 $grid-unit-30 #{ $grid-unit-60 * 2 } $grid-unit-30; padding: 0 $grid-unit-30 $grid-unit-30 $grid-unit-30; } - } // Exit code editor toolbar. @@ -70,8 +68,4 @@ font-size: $default-font-size; color: $gray-900; } - - .components-button svg { - order: 1; - } } diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 475b2aac9478bf..d1c7cdbade44f8 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -14,7 +14,6 @@ import { SlotFillProvider } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; import { CommandMenu } from '@wordpress/commands'; -import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -23,23 +22,28 @@ import Layout from './components/layout'; import EditorInitialization from './components/editor-initialization'; import { store as editPostStore } from './store'; import { unlock } from './lock-unlock'; +import usePostHistory from './hooks/use-post-history'; const { ExperimentalEditorProvider } = unlock( editorPrivateApis ); -function Editor( { postId, postType, settings, initialEdits, ...props } ) { - const isLargeViewport = useViewportMatch( 'medium' ); +function Editor( { + postId: initialPostId, + postType: initialPostType, + settings, + initialEdits, + ...props +} ) { + const { currentPost, getPostLinkProps, goBack } = usePostHistory( + initialPostId, + initialPostType + ); const { - allowRightClickOverrides, - hasFixedToolbar, - focusMode, - isDistractionFree, hasInlineToolbar, post, preferredStyleVariations, hiddenBlockTypes, blockTypes, - keepCaretInsideBlock, template, } = useSelect( ( select ) => { @@ -53,31 +57,33 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { const { getEditorSettings } = select( editorStore ); const { getBlockTypes } = select( blocksStore ); const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( - postType + currentPost.postType ); // Ideally the initializeEditor function should be called using the ID of the REST endpoint. // to avoid the special case. let postObject; if ( isTemplate ) { - const posts = getEntityRecords( 'postType', postType, { - wp_id: postId, - } ); + const posts = getEntityRecords( + 'postType', + currentPost.postType, + { + wp_id: currentPost.postId, + } + ); postObject = posts?.[ 0 ]; } else { - postObject = getEntityRecord( 'postType', postType, postId ); + postObject = getEntityRecord( + 'postType', + currentPost.postType, + currentPost.postId + ); } const supportsTemplateMode = getEditorSettings().supportsTemplateMode; - const isViewable = getPostType( postType )?.viewable ?? false; + const isViewable = + getPostType( currentPost.postType )?.viewable ?? false; const canEditTemplate = canUser( 'create', 'templates' ); return { - allowRightClickOverrides: isFeatureActive( - 'allowRightClickOverrides' - ), - hasFixedToolbar: - isFeatureActive( 'fixedToolbar' ) || ! isLargeViewport, - focusMode: isFeatureActive( 'focusMode' ), - isDistractionFree: isFeatureActive( 'distractionFree' ), hasInlineToolbar: isFeatureActive( 'inlineToolbar' ), preferredStyleVariations: select( preferencesStore ).get( 'core/edit-post', @@ -85,7 +91,6 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { ), hiddenBlockTypes: getHiddenBlockTypes(), blockTypes: getBlockTypes(), - keepCaretInsideBlock: isFeatureActive( 'keepCaretInsideBlock' ), template: supportsTemplateMode && isViewable && canEditTemplate ? getEditedPostTemplate() @@ -93,7 +98,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { post: postObject, }; }, - [ postType, postId, isLargeViewport ] + [ currentPost.postType, currentPost.postId ] ); const { updatePreferredStyleVariations } = useDispatch( editPostStore ); @@ -101,17 +106,14 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { const editorSettings = useMemo( () => { const result = { ...settings, + getPostLinkProps, + goBack, __experimentalPreferredStyleVariations: { value: preferredStyleVariations, onChange: updatePreferredStyleVariations, }, - hasFixedToolbar, - focusMode, - isDistractionFree, hasInlineToolbar, - allowRightClickOverrides, - keepCaretInsideBlock, // Keep a reference of the `allowedBlockTypes` from the server to handle use cases // where we need to differentiate if a block is disabled by the user or some plugin. defaultAllowedBlockTypes: settings.allowedBlockTypes, @@ -135,16 +137,13 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { return result; }, [ settings, - allowRightClickOverrides, - hasFixedToolbar, hasInlineToolbar, - focusMode, - isDistractionFree, hiddenBlockTypes, blockTypes, preferredStyleVariations, updatePreferredStyleVariations, - keepCaretInsideBlock, + getPostLinkProps, + goBack, ] ); if ( ! post ) { @@ -163,7 +162,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { > - + diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index 17e2ad1780f1d7..74d4299a2a1fd1 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -47,18 +47,10 @@ class Editor extends Component { this.setTitleRef = this.setTitleRef.bind( this ); } - getEditorSettings( - settings, - hasFixedToolbar, - focusMode, - hiddenBlockTypes, - blockTypes - ) { + getEditorSettings( settings, hiddenBlockTypes, blockTypes ) { settings = { ...settings, isRTL: I18nManager.isRTL, - hasFixedToolbar, - focusMode, }; // Omit hidden block types if exists and non-empty. @@ -134,8 +126,6 @@ class Editor extends Component { render() { const { settings, - hasFixedToolbar, - focusMode, initialEdits, hiddenBlockTypes, blockTypes, @@ -149,8 +139,6 @@ class Editor extends Component { const editorSettings = this.getEditorSettings( settings, - hasFixedToolbar, - focusMode, hiddenBlockTypes, blockTypes ); @@ -192,13 +180,10 @@ class Editor extends Component { export default compose( [ withSelect( ( select ) => { - const { isFeatureActive, getEditorMode, getHiddenBlockTypes } = - select( editPostStore ); + const { getEditorMode, getHiddenBlockTypes } = select( editPostStore ); const { getBlockTypes } = select( blocksStore ); return { - hasFixedToolbar: isFeatureActive( 'fixedToolbar' ), - focusMode: isFeatureActive( 'focusMode' ), mode: getEditorMode(), hiddenBlockTypes: getHiddenBlockTypes(), blockTypes: getBlockTypes(), diff --git a/packages/edit-post/src/hooks/commands/use-common-commands.js b/packages/edit-post/src/hooks/commands/use-common-commands.js index 0f6959d3b813be..003f84349dfa90 100644 --- a/packages/edit-post/src/hooks/commands/use-common-commands.js +++ b/packages/edit-post/src/hooks/commands/use-common-commands.js @@ -43,7 +43,8 @@ export default function useCommonCommands() { showBlockBreadcrumbs, isDistractionFree, } = useSelect( ( select ) => { - const { getEditorMode, isFeatureActive } = select( editPostStore ); + const { get } = select( preferencesStore ); + const { getEditorMode } = select( editPostStore ); const { isListViewOpened } = select( editorStore ); return { activeSidebar: select( interfaceStore ).getActiveComplementaryArea( @@ -53,11 +54,8 @@ export default function useCommonCommands() { isListViewOpen: isListViewOpened(), isPublishSidebarEnabled: select( editorStore ).isPublishSidebarEnabled(), - showBlockBreadcrumbs: isFeatureActive( 'showBlockBreadcrumbs' ), - isDistractionFree: select( preferencesStore ).get( - editPostStore.name, - 'distractionFree' - ), + showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), + isDistractionFree: get( 'core', 'distractionFree' ), }; }, [] ); const { toggle } = useDispatch( preferencesStore ); @@ -107,7 +105,7 @@ export default function useCommonCommands() { name: 'core/toggle-spotlight-mode', label: __( 'Toggle spotlight mode' ), callback: ( { close } ) => { - toggle( 'core/edit-post', 'focusMode' ); + toggle( 'core', 'focusMode' ); close(); }, } ); @@ -136,7 +134,7 @@ export default function useCommonCommands() { name: 'core/toggle-top-toolbar', label: __( 'Toggle top toolbar' ), callback: ( { close } ) => { - toggle( 'core/edit-post', 'fixedToolbar' ); + toggle( 'core', 'fixedToolbar' ); if ( isDistractionFree ) { toggleDistractionFree(); } @@ -177,7 +175,7 @@ export default function useCommonCommands() { ? __( 'Hide block breadcrumbs' ) : __( 'Show block breadcrumbs' ), callback: ( { close } ) => { - toggle( 'core/edit-post', 'showBlockBreadcrumbs' ); + toggle( 'core', 'showBlockBreadcrumbs' ); close(); createInfoNotice( showBlockBreadcrumbs @@ -194,16 +192,16 @@ export default function useCommonCommands() { useCommand( { name: 'core/toggle-publish-sidebar', label: isPublishSidebarEnabled - ? __( 'Disable pre-publish checklist' ) - : __( 'Enable pre-publish checklist' ), + ? __( 'Disable pre-publish checks' ) + : __( 'Enable pre-publish checks' ), icon: formatListBullets, callback: ( { close } ) => { close(); toggle( 'core/edit-post', 'isPublishSidebarEnabled' ); createInfoNotice( isPublishSidebarEnabled - ? __( 'Pre-publish checklist off.' ) - : __( 'Pre-publish checklist on.' ), + ? __( 'Pre-publish checks disabled.' ) + : __( 'Pre-publish checks enabled.' ), { id: 'core/edit-post/publish-sidebar/notice', type: 'snackbar', diff --git a/packages/edit-post/src/hooks/use-post-history.js b/packages/edit-post/src/hooks/use-post-history.js new file mode 100644 index 00000000000000..02c34a26727b19 --- /dev/null +++ b/packages/edit-post/src/hooks/use-post-history.js @@ -0,0 +1,73 @@ +/** + * WordPress dependencies + */ +import { useCallback, useReducer } from '@wordpress/element'; +import { addQueryArgs, getQueryArgs, removeQueryArgs } from '@wordpress/url'; + +/** + * A hook that records the 'entity' history in the post editor as a user + * navigates between editing a post and editing the post template or patterns. + * + * Implemented as a stack, so a little similar to the browser history API. + * + * Used to control displaying UI elements like the back button. + * + * @param {number} initialPostId The post id of the post when the editor loaded. + * @param {string} initialPostType The post type of the post when the editor loaded. + * + * @return {Object} An object containing the `currentPost` variable and + * `getPostLinkProps` and `goBack` functions. + */ +export default function usePostHistory( initialPostId, initialPostType ) { + const [ postHistory, dispatch ] = useReducer( + ( historyState, { type, post } ) => { + if ( type === 'push' ) { + return [ ...historyState, post ]; + } + if ( type === 'pop' ) { + // Try to leave one item in the history. + if ( historyState.length > 1 ) { + return historyState.slice( 0, -1 ); + } + } + return historyState; + }, + [ { postId: initialPostId, postType: initialPostType } ] + ); + + const getPostLinkProps = useCallback( ( params ) => { + const currentArgs = getQueryArgs( window.location.href ); + const currentUrlWithoutArgs = removeQueryArgs( + window.location.href, + ...Object.keys( currentArgs ) + ); + + const newUrl = addQueryArgs( currentUrlWithoutArgs, { + post: params.postId, + action: 'edit', + } ); + + return { + href: newUrl, + onClick: ( event ) => { + event.preventDefault(); + dispatch( { + type: 'push', + post: { postId: params.postId, postType: params.postType }, + } ); + }, + }; + }, [] ); + + const goBack = useCallback( () => { + dispatch( { type: 'pop' } ); + }, [] ); + + const currentPost = postHistory[ postHistory.length - 1 ]; + + return { + currentPost, + getPostLinkProps, + goBack: postHistory.length > 1 ? goBack : undefined, + }; +} diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 38848f95efa8e7..a3831047457be9 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -26,7 +26,6 @@ import { import './hooks'; import './plugins'; import Editor from './editor'; -import { store as editPostStore } from './store'; import { unlock } from './lock-unlock'; const { PluginPostExcerpt: __experimentalPluginPostExcerpt } = @@ -54,30 +53,33 @@ export function initializeEditor( const root = createRoot( target ); dispatch( preferencesStore ).setDefaults( 'core/edit-post', { - allowRightClickOverrides: true, editorMode: 'visual', - fixedToolbar: false, fullscreenMode: true, hiddenBlockTypes: [], - inactivePanels: [], isPublishSidebarEnabled: true, - openPanels: [ 'post-status' ], preferredStyleVariations: {}, - showBlockBreadcrumbs: true, - showIconLabels: false, - showListViewByDefault: false, themeStyles: true, welcomeGuide: true, welcomeGuideTemplate: true, } ); + dispatch( preferencesStore ).setDefaults( 'core', { + allowRightClickOverrides: true, + fixedToolbar: false, + inactivePanels: [], + openPanels: [ 'post-status' ], + showBlockBreadcrumbs: true, + showIconLabels: false, + showListViewByDefault: false, + } ); + dispatch( blocksStore ).reapplyBlockTypeFilters(); // Check if the block list view should be open by default. // If `distractionFree` mode is enabled, the block list view should not be open. if ( - select( editPostStore ).isFeatureActive( 'showListViewByDefault' ) && - ! select( editPostStore ).isFeatureActive( 'distractionFree' ) + select( preferencesStore ).get( 'core', 'showListViewByDefault' ) && + ! select( preferencesStore ).get( 'core', 'distractionFree' ) ) { dispatch( editorStore ).setIsListViewOpened( true ); } diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index b825d50ace0c01..974f617d609f23 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -23,7 +23,6 @@ import Editor from './editor'; export function initializeEditor( id, postType, postId ) { dispatch( preferencesStore ).setDefaults( 'core/edit-post', { editorMode: 'visual', - fixedToolbar: false, fullscreenMode: true, hiddenBlockTypes: [], inactivePanels: [], @@ -32,6 +31,14 @@ export function initializeEditor( id, postType, postId ) { preferredStyleVariations: {}, welcomeGuide: true, } ); + dispatch( preferencesStore ).setDefaults( 'core', { + inactivePanels: [], + openPanels: [ 'post-status' ], + } ); + + dispatch( preferencesStore ).setDefaults( 'core', { + fixedToolbar: false, + } ); return ; } diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 8bf784aad16d5d..a4ca78f95e59c2 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -28,7 +28,7 @@ export const openGeneralSidebar = ( { dispatch, registry } ) => { const isDistractionFree = registry .select( preferencesStore ) - .get( 'core/edit-post', 'distractionFree' ); + .get( 'core', 'distractionFree' ); if ( isDistractionFree ) { dispatch.toggleDistractionFree(); } @@ -205,9 +205,7 @@ export const switchEditorMode = if ( mode === 'text' && - registry - .select( preferencesStore ) - .get( 'core/edit-post', 'distractionFree' ) + registry.select( preferencesStore ).get( 'core', 'distractionFree' ) ) { dispatch.toggleDistractionFree(); } @@ -580,12 +578,12 @@ export const toggleDistractionFree = ( { dispatch, registry } ) => { const isDistractionFree = registry .select( preferencesStore ) - .get( 'core/edit-post', 'distractionFree' ); + .get( 'core', 'distractionFree' ); if ( ! isDistractionFree ) { registry.batch( () => { registry .dispatch( preferencesStore ) - .set( 'core/edit-post', 'fixedToolbar', true ); + .set( 'core', 'fixedToolbar', true ); registry.dispatch( editorStore ).setIsInserterOpened( false ); registry.dispatch( editorStore ).setIsListViewOpened( false ); dispatch.closeGeneralSidebar(); @@ -594,11 +592,7 @@ export const toggleDistractionFree = registry.batch( () => { registry .dispatch( preferencesStore ) - .set( - 'core/edit-post', - 'distractionFree', - ! isDistractionFree - ); + .set( 'core', 'distractionFree', ! isDistractionFree ); registry .dispatch( noticesStore ) .createInfoNotice( diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index dff5f27c8918af..0d879493f995b9 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -186,13 +186,10 @@ export const getPreferences = createRegistrySelector( ( select ) => () => { // the new preferences store format to old format to ensure no breaking // changes for plugins. const inactivePanels = select( preferencesStore ).get( - 'core/edit-post', + 'core', 'inactivePanels' ); - const openPanels = select( preferencesStore ).get( - 'core/edit-post', - 'openPanels' - ); + const openPanels = select( preferencesStore ).get( 'core', 'openPanels' ); const panels = convertPanelsToOldFormat( inactivePanels, openPanels ); return { diff --git a/packages/edit-post/src/store/test/actions.js b/packages/edit-post/src/store/test/actions.js index 82c80eb0f5273e..f702d412d55dad 100644 --- a/packages/edit-post/src/store/test/actions.js +++ b/packages/edit-post/src/store/test/actions.js @@ -56,14 +56,12 @@ describe( 'actions', () => { it( 'openGeneralSidebar - should turn off distraction free mode when opening a general sidebar', () => { registry .dispatch( preferencesStore ) - .set( 'core/edit-post', 'distractionFree', true ); + .set( 'core', 'distractionFree', true ); registry .dispatch( editPostStore ) .openGeneralSidebar( 'edit-post/block' ); expect( - registry - .select( preferencesStore ) - .get( 'core/edit-post', 'distractionFree' ) + registry.select( preferencesStore ).get( 'core', 'distractionFree' ) ).toBe( false ); } ); @@ -119,12 +117,12 @@ describe( 'actions', () => { it( 'should turn off distraction free mode when switching to code editor', () => { registry .dispatch( preferencesStore ) - .set( 'core/edit-post', 'distractionFree', true ); + .set( 'core', 'distractionFree', true ); registry.dispatch( editPostStore ).switchEditorMode( 'text' ); expect( registry .select( preferencesStore ) - .get( 'core/edit-post', 'distractionFree' ) + .get( 'core', 'distractionFree' ) ).toBe( false ); } ); } ); @@ -259,7 +257,7 @@ describe( 'actions', () => { // Enable everything that shouldn't be enabled in distraction free mode. registry .dispatch( preferencesStore ) - .set( 'core/edit-post', 'fixedToolbar', true ); + .set( 'core', 'fixedToolbar', true ); registry.dispatch( editorStore ).setIsListViewOpened( true ); registry .dispatch( editPostStore ) @@ -269,7 +267,7 @@ describe( 'actions', () => { expect( registry .select( preferencesStore ) - .get( 'core/edit-post', 'fixedToolbar' ) + .get( 'core', 'fixedToolbar' ) ).toBe( true ); expect( registry.select( editorStore ).isListViewOpened() ).toBe( false @@ -285,7 +283,7 @@ describe( 'actions', () => { expect( registry .select( preferencesStore ) - .get( 'core/edit-post', 'distractionFree' ) + .get( 'core', 'distractionFree' ) ).toBe( true ); } ); } ); diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index fe03b2f7133735..adfd7218c4c123 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -1,12 +1,10 @@ @import "../../interface/src/style.scss"; @import "./components/header/style.scss"; @import "./components/header/fullscreen-mode-close/style.scss"; -@import "./components/header/header-toolbar/style.scss"; @import "./components/keyboard-shortcut-help-modal/style.scss"; @import "./components/layout/style.scss"; @import "./components/block-manager/style.scss"; @import "./components/meta-boxes/meta-boxes-area/style.scss"; -@import "./components/secondary-sidebar/style.scss"; @import "./components/sidebar/style.scss"; @import "./components/sidebar/post-format/style.scss"; @import "./components/sidebar/post-slug/style.scss"; @@ -51,8 +49,7 @@ body.js.block-editor-page { .edit-post-sidebar, .editor-post-publish-panel, .components-popover, -.components-modal__frame, -.edit-post-editor__inserter-panel { +.components-modal__frame { @include reset; } diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index 7faeb2e51ab4b2..20eacc4bdf1db3 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -11,6 +11,7 @@ import { screen, setupCoreBlocks, } from 'test/helpers'; +import { BackHandler } from 'react-native'; /** * WordPress dependencies @@ -129,4 +130,20 @@ describe( 'Editor', () => { // Assert expect( getEditorHtml() ).toMatchSnapshot(); } ); + + it( 'unselects current block when tapping on the hardware back button', async () => { + // Arrange + await initializeEditor(); + await addBlock( screen, 'Spacer' ); + + // Act + act( () => { + BackHandler.mockPressBack(); + } ); + + // Assert + const openBlockSettingsButton = + screen.queryAllByLabelText( 'Open Settings' ); + expect( openBlockSettingsButton.length ).toBe( 0 ); + } ); } ); diff --git a/packages/edit-site/CHANGELOG.md b/packages/edit-site/CHANGELOG.md index 718716e6a9a3d6..ff3e85639d1dee 100644 --- a/packages/edit-site/CHANGELOG.md +++ b/packages/edit-site/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 5.26.0 (2023-12-27) - ## 5.25.0 (2023-12-13) ## 5.24.0 (2023-11-29) diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 0debc2609e6ca4..e560e5e6827978 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "5.26.0-prerelease", + "version": "5.25.0", "description": "Edit Site Page module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/src/components/add-new-template/style.scss b/packages/edit-site/src/components/add-new-template/style.scss index b1c2b669e24cef..4881745c92afae 100644 --- a/packages/edit-site/src/components/add-new-template/style.scss +++ b/packages/edit-site/src/components/add-new-template/style.scss @@ -39,7 +39,8 @@ .edit-site-custom-template-modal__suggestions_list { @include break-small() { - overflow: scroll; + max-height: $grid-unit-70 * 4; // Height of four buttons + overflow-y: auto; } &__list-item { diff --git a/packages/edit-site/src/components/block-editor/back-button.js b/packages/edit-site/src/components/block-editor/back-button.js index 924dedd4f853e1..acd9cf7028e658 100644 --- a/packages/edit-site/src/components/block-editor/back-button.js +++ b/packages/edit-site/src/components/block-editor/back-button.js @@ -12,6 +12,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import { TEMPLATE_PART_POST_TYPE, NAVIGATION_POST_TYPE, + PATTERN_TYPES, } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; @@ -22,11 +23,12 @@ function BackButton() { const history = useHistory(); const isTemplatePart = location.params.postType === TEMPLATE_PART_POST_TYPE; const isNavigationMenu = location.params.postType === NAVIGATION_POST_TYPE; + const isPattern = location.params.postType === PATTERN_TYPES.user; const previousTemplateId = location.state?.fromTemplateId; - const isFocusMode = isTemplatePart || isNavigationMenu; + const isFocusMode = isTemplatePart || isNavigationMenu || isPattern; - if ( ! isFocusMode || ! previousTemplateId ) { + if ( ! isFocusMode || ( ! previousTemplateId && ! isPattern ) ) { return null; } diff --git a/packages/edit-site/src/components/block-editor/resizable-editor.js b/packages/edit-site/src/components/block-editor/resizable-editor.js index 20fdd972528439..eda5848cff2063 100644 --- a/packages/edit-site/src/components/block-editor/resizable-editor.js +++ b/packages/edit-site/src/components/block-editor/resizable-editor.js @@ -46,8 +46,8 @@ function ResizableEditor( { enableResizing, height, children } ) { maxWidth="100%" maxHeight="100%" enable={ { - right: enableResizing, left: enableResizing, + right: enableResizing, } } showHandle={ enableResizing } // The editor is centered horizontally, resizing it only diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index 1da43730d95753..6294853b5c0a87 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -27,6 +27,7 @@ // Centralize the editor horizontally (flex-direction is column). align-items: center; + // Controls height of editor and editor canvas container (style book, global styles revisions previews etc.) iframe { display: block; width: 100%; @@ -35,8 +36,6 @@ } .edit-site-visual-editor__editor-canvas { - height: 100%; - &.is-focused { outline: calc(2 * var(--wp-admin-border-width-focus)) solid var(--wp-admin-theme-color); outline-offset: calc(-2 * var(--wp-admin-border-width-focus)); diff --git a/packages/edit-site/src/components/block-editor/use-post-link-props.js b/packages/edit-site/src/components/block-editor/use-post-link-props.js new file mode 100644 index 00000000000000..dd023053931224 --- /dev/null +++ b/packages/edit-site/src/components/block-editor/use-post-link-props.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { getPostLinkProps } from '../routes/link'; + +const { useHistory } = unlock( routerPrivateApis ); + +export function usePostLinkProps() { + const history = useHistory(); + + return ( params, state ) => { + return getPostLinkProps( history, params, state ); + }; +} diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 3cd65802b29de5..61c2232df6f8e0 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -1,18 +1,17 @@ /** * WordPress dependencies */ -import { useViewportMatch } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; -import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies */ import { store as editSiteStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import { usePostLinkProps } from './use-post-link-props'; const { useBlockEditorSettings } = unlock( editorPrivateApis ); @@ -89,18 +88,8 @@ function useArchiveLabel( templateSlug ) { } export function useSpecificEditorSettings() { - const isLargeViewport = useViewportMatch( 'medium' ); - const { - templateSlug, - focusMode, - allowRightClickOverrides, - isDistractionFree, - hasFixedToolbar, - keepCaretInsideBlock, - canvasMode, - settings, - postWithTemplate, - } = useSelect( + const getPostLinkProps = usePostLinkProps(); + const { templateSlug, canvasMode, settings, postWithTemplate } = useSelect( ( select ) => { const { getEditedPostType, @@ -109,7 +98,6 @@ export function useSpecificEditorSettings() { getCanvasMode, getSettings, } = unlock( select( editSiteStore ) ); - const { get: getPreference } = select( preferencesStore ); const { getEditedEntityRecord } = select( coreStore ); const usedPostType = getEditedPostType(); const usedPostId = getEditedPostId(); @@ -121,28 +109,12 @@ export function useSpecificEditorSettings() { const _context = getEditedPostContext(); return { templateSlug: _record.slug, - focusMode: !! getPreference( 'core/edit-site', 'focusMode' ), - isDistractionFree: !! getPreference( - 'core/edit-site', - 'distractionFree' - ), - allowRightClickOverrides: !! getPreference( - 'core/edit-site', - 'allowRightClickOverrides' - ), - hasFixedToolbar: - !! getPreference( 'core/edit-site', 'fixedToolbar' ) || - ! isLargeViewport, - keepCaretInsideBlock: !! getPreference( - 'core/edit-site', - 'keepCaretInsideBlock' - ), canvasMode: getCanvasMode(), settings: getSettings(), postWithTemplate: _context?.postId, }; }, - [ isLargeViewport ] + [] ); const archiveLabels = useArchiveLabel( templateSlug ); const defaultRenderingMode = postWithTemplate ? 'template-locked' : 'all'; @@ -150,29 +122,22 @@ export function useSpecificEditorSettings() { return { ...settings, + richEditingEnabled: true, supportsTemplateMode: true, - focusMode: canvasMode === 'view' && focusMode ? false : focusMode, - allowRightClickOverrides, - isDistractionFree, - hasFixedToolbar, - keepCaretInsideBlock, + focusMode: canvasMode !== 'view', defaultRenderingMode, - + getPostLinkProps, // I wonder if they should be set in the post editor too __experimentalArchiveTitleTypeLabel: archiveLabels.archiveTypeLabel, __experimentalArchiveTitleNameLabel: archiveLabels.archiveNameLabel, }; }, [ settings, - focusMode, - allowRightClickOverrides, - isDistractionFree, - hasFixedToolbar, - keepCaretInsideBlock, canvasMode, + defaultRenderingMode, + getPostLinkProps, archiveLabels.archiveTypeLabel, archiveLabels.archiveNameLabel, - defaultRenderingMode, ] ); return defaultEditorSettings; diff --git a/packages/edit-site/src/components/code-editor/style.scss b/packages/edit-site/src/components/code-editor/style.scss index 0e79575c49f678..17431de27b896c 100644 --- a/packages/edit-site/src/components/code-editor/style.scss +++ b/packages/edit-site/src/components/code-editor/style.scss @@ -41,10 +41,6 @@ font-size: $default-font-size; color: $gray-900; } - - .components-button svg { - order: 1; - } } } diff --git a/packages/edit-site/src/components/create-template-part-modal/index.js b/packages/edit-site/src/components/create-template-part-modal/index.js index 31f12b6cab56d3..b44446da0c0686 100644 --- a/packages/edit-site/src/components/create-template-part-modal/index.js +++ b/packages/edit-site/src/components/create-template-part-modal/index.js @@ -39,11 +39,25 @@ import { } from '../../utils/template-part-create'; export default function CreateTemplatePartModal( { + modalTitle = __( 'Create template part' ), + ...restProps +} ) { + return ( + + + + ); +} + +export function CreateTemplatePartModalContents( { defaultArea = TEMPLATE_PART_AREA_DEFAULT_CATEGORY, blocks = [], confirmLabel = __( 'Create' ), closeModal, - modalTitle = __( 'Create template part' ), onCreate, onError, defaultTitle = '', @@ -62,7 +76,6 @@ export default function CreateTemplatePartModal( { select( editorStore ).__experimentalGetDefaultTemplatePartAreas(), [] ); - async function createTemplatePart() { if ( ! title || isSubmitting ) { return; @@ -105,91 +118,79 @@ export default function CreateTemplatePartModal( { setIsSubmitting( false ); } } - return ( - { + event.preventDefault(); + await createTemplatePart(); + } } > -
{ - event.preventDefault(); - await createTemplatePart(); - } } - > - - - + + + - - { templatePartAreas.map( - ( { - icon, - label, - area: value, - description, - } ) => ( - - - - - - - { label } -
{ description }
-
+ { templatePartAreas.map( + ( { icon, label, area: value, description } ) => ( + + + + + + + { label } +
{ description }
+
- - { area === value && ( - - ) } - -
-
- ) - ) } -
-
- - - - -
-
-
+ + { area === value && ( + + ) } + + + + ) + ) } + + + + + + + + ); } diff --git a/packages/edit-site/src/components/editor-canvas-container/index.js b/packages/edit-site/src/components/editor-canvas-container/index.js index d08c30ac5646ab..5bb7fc72a9c0f5 100644 --- a/packages/edit-site/src/components/editor-canvas-container/index.js +++ b/packages/edit-site/src/components/editor-canvas-container/index.js @@ -63,7 +63,7 @@ function EditorCanvasContainer( { ).getEditorCanvasContainerView(); const _showListViewByDefault = select( preferencesStore ).get( - 'core/edit-site', + 'core', 'showListViewByDefault' ); diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index afae1b362ee998..ffb9c5446d7963 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -42,8 +42,6 @@ import { } from '../sidebar-edit-mode'; import CodeEditor from '../code-editor'; import KeyboardShortcutsEditMode from '../keyboard-shortcuts/edit-mode'; -import InserterSidebar from '../secondary-sidebar/inserter-sidebar'; -import ListViewSidebar from '../secondary-sidebar/list-view-sidebar'; import WelcomeGuide from '../welcome-guide'; import StartTemplateOptions from '../start-template-options'; import { store as editSiteStore } from '../../store'; @@ -59,8 +57,11 @@ 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 { + ExperimentalEditorProvider: EditorProvider, + InserterSidebar, + ListViewSidebar, +} = unlock( editorPrivateApis ); const interfaceLabels = { /* translators: accessibility text for the editor content landmark region. */ @@ -109,6 +110,7 @@ export default function Editor( { isLoading } ) { showIconLabels, showBlockBreadcrumbs, } = useSelect( ( select ) => { + const { get } = select( preferencesStore ); const { getEditedPostContext, getEditorMode, getCanvasMode } = unlock( select( editSiteStore ) ); @@ -139,14 +141,8 @@ export default function Editor( { isLoading } ) { isRightSidebarOpen: getActiveComplementaryArea( editSiteStore.name ), - showIconLabels: select( preferencesStore ).get( - 'core/edit-site', - 'showIconLabels' - ), - showBlockBreadcrumbs: select( preferencesStore ).get( - 'core/edit-site', - 'showBlockBreadcrumbs' - ), + showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), + showIconLabels: get( 'core', 'showIconLabels' ), }; }, [] ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/confirm-delete-dialog.js b/packages/edit-site/src/components/global-styles/font-library-modal/confirm-delete-dialog.js index 259b6900dd16d5..b87a921bd35e59 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/confirm-delete-dialog.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/confirm-delete-dialog.js @@ -13,8 +13,8 @@ function ConfirmDeleteDialog( { return ( @@ -22,7 +22,7 @@ function ConfirmDeleteDialog( { sprintf( /* translators: %s: Name of the font. */ __( - 'Would you like to remove %s and all its variants and assets?' + 'Are you sure you want to delete "%s" font and all its variants and assets?' ), font.name ) } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 58b8621adcf0c3..e0749845788d60 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -14,7 +14,7 @@ import { * Internal dependencies */ import { - fetchInstallFonts, + fetchInstallFont, fetchUninstallFonts, fetchFontCollections, fetchFontCollection, @@ -26,7 +26,7 @@ import { mergeFontFamilies, loadFontFaceInBrowser, getDisplaySrcFromFontFace, - makeFormDataFromFontFamilies, + makeFormDataFromFontFamily, } from './utils'; import { toggleFont } from './utils/toggleFont'; import getIntersectingFontFaces from './utils/get-intersecting-font-faces'; @@ -192,19 +192,19 @@ function FontLibraryProvider( { children } ) { return getActivatedFontsOutline( source )[ slug ] || []; }; - async function installFonts( fonts ) { + async function installFont( font ) { setIsInstalling( true ); try { // Prepare formData to install. - const formData = makeFormDataFromFontFamilies( fonts ); + const formData = makeFormDataFromFontFamily( font ); // Install the fonts (upload the font files to the server and create the post in the database). - const response = await fetchInstallFonts( formData ); + const response = await fetchInstallFont( formData ); const fontsInstalled = response?.successes || []; // Get intersecting font faces between the fonts we tried to installed and the fonts that were installed // (to avoid activating a non installed font). const fontToBeActivated = getIntersectingFontFaces( fontsInstalled, - fonts + [ font ] ); // Activate the font families (add the font families to the global styles). activateCustomFontFamilies( fontToBeActivated ); @@ -358,7 +358,7 @@ function FontLibraryProvider( { children } ) { isFontActivated, getFontFacesActivated, loadFontFaceAsset, - installFonts, + installFont, uninstallFont, toggleActivateFont, getAvailableFontsOutline, diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index 08ff1a95d6e41a..fc39e2e0096531 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -21,7 +21,7 @@ import { search, closeSmall } from '@wordpress/icons'; /** * Internal dependencies */ -import TabLayout from './tab-layout'; +import TabPanelLayout from './tab-panel-layout'; import { FontLibraryContext } from './context'; import FontsGrid from './fonts-grid'; import FontCard from './font-card'; @@ -54,7 +54,7 @@ function FontCollection( { id } ) { const [ renderConfirmDialog, setRenderConfirmDialog ] = useState( requiresPermission && ! getGoogleFontsPermissionFromStorage() ); - const { collections, getFontCollection, installFonts } = + const { collections, getFontCollection, installFont } = useContext( FontLibraryContext ); const selectedCollection = collections.find( ( collection ) => collection.id === id @@ -92,6 +92,11 @@ function FontCollection( { id } ) { setNotice( null ); }, [ id ] ); + useEffect( () => { + // If the selected fonts change, reset the selected fonts to install + setFontsToInstall( [] ); + }, [ selectedFont ] ); + // Reset notice after 5 seconds useEffect( () => { if ( notice && notice?.duration !== 0 ) { @@ -149,14 +154,14 @@ function FontCollection( { id } ) { }; const handleInstall = async () => { - const response = await installFonts( fontsToInstall ); + const response = await installFont( fontsToInstall[ 0 ] ); const installNotice = getNoticeFromInstallResponse( response ); setNotice( installNotice ); resetFontsToInstall(); }; return ( - ) } - + ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js index 1128ca0811977e..65a284560687cc 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js @@ -2,7 +2,10 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Modal, TabPanel } from '@wordpress/components'; +import { + Modal, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; import { useContext } from '@wordpress/element'; /** @@ -12,33 +15,33 @@ import InstalledFonts from './installed-fonts'; import FontCollection from './font-collection'; import UploadFonts from './upload-fonts'; import { FontLibraryContext } from './context'; +import { unlock } from '../../../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); const DEFAULT_TABS = [ { - name: 'installed-fonts', + id: 'installed-fonts', title: __( 'Library' ), - className: 'installed-fonts', }, { - name: 'upload-fonts', + id: 'upload-fonts', title: __( 'Upload' ), - className: 'upload-fonts', }, ]; const tabsFromCollections = ( collections ) => collections.map( ( { id, name } ) => ( { - name: id, + id, title: collections.length === 1 && id === 'default-font-collection' ? __( 'Install Fonts' ) : name, - className: 'collection', } ) ); function FontLibraryModal( { onRequestClose, - initialTabName = 'installed-fonts', + initialTabId = 'installed-fonts', } ) { const { collections } = useContext( FontLibraryContext ); @@ -54,22 +57,39 @@ function FontLibraryModal( { isFullScreen className="font-library-modal" > - - { ( tab ) => { - switch ( tab.name ) { - case 'upload-fonts': - return ; - case 'installed-fonts': - return ; - default: - return ; - } - } } - +
+ + + { tabs.map( ( { id, title } ) => ( + + { title } + + ) ) } + + { tabs.map( ( { id } ) => { + let contents; + switch ( id ) { + case 'upload-fonts': + contents = ; + break; + case 'installed-fonts': + contents = ; + break; + default: + contents = ; + } + return ( + + { contents } + + ); + } ) } + +
); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index 2a8d1e591e084f..d493a2a297b18b 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -16,7 +16,7 @@ import { /** * Internal dependencies */ -import TabLayout from './tab-layout'; +import TabPanelLayout from './tab-panel-layout'; import { FontLibraryContext } from './context'; import FontsGrid from './fonts-grid'; import LibraryFontDetails from './library-font-details'; @@ -92,7 +92,7 @@ function InstalledFonts() { }, [ notice ] ); return ( - ) } - + ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js index 9612f8be52f5ee..d4221b420cb613 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/local-fonts.js @@ -11,6 +11,7 @@ import { FormFileUpload, Notice, FlexItem, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { useContext, useState, useEffect } from '@wordpress/element'; @@ -23,10 +24,14 @@ import { Font } from '../../../../lib/lib-font.browser'; import makeFamiliesFromFaces from './utils/make-families-from-faces'; import { loadFontFaceInBrowser } from './utils'; import { getNoticeFromInstallResponse } from './utils/get-notice-from-response'; +import { unlock } from '../../../lock-unlock'; + +const { ProgressBar } = unlock( componentsPrivateApis ); function LocalFonts() { - const { installFonts } = useContext( FontLibraryContext ); + const { installFont } = useContext( FontLibraryContext ); const [ notice, setNotice ] = useState( null ); + const [ isUploading, setIsUploading ] = useState( false ); const supportedFormats = ALLOWED_FILE_EXTENSIONS.slice( 0, -1 ) .map( ( extension ) => `.${ extension }` ) @@ -58,6 +63,7 @@ function LocalFonts() { */ const handleFilesUpload = ( files ) => { setNotice( null ); + setIsUploading( true ); const uniqueFilenames = new Set(); const selectedFiles = [ ...files ]; const allowedFiles = selectedFiles.filter( ( file ) => { @@ -147,9 +153,21 @@ function LocalFonts() { */ const handleInstall = async ( fontFaces ) => { const fontFamilies = makeFamiliesFromFaces( fontFaces ); - const response = await installFonts( fontFamilies ); + + if ( fontFamilies.length > 1 ) { + setNotice( { + type: 'error', + message: __( + 'Variants from only one font family can be uploaded at a time.' + ), + } ); + return; + } + + const response = await installFont( fontFamilies[ 0 ] ); const installNotice = getNoticeFromInstallResponse( response ); setNotice( installNotice ); + setIsUploading( false ); }; return ( @@ -157,31 +175,28 @@ function LocalFonts() { - `.${ ext }` - ).join( ',' ) } - multiple={ true } - onChange={ onFilesUpload } - render={ ( { openFileDialog } ) => ( - - ) } - /> - { notice && ( + { ! isUploading && ( + `.${ ext }` + ).join( ',' ) } + multiple={ true } + onChange={ onFilesUpload } + render={ ( { openFileDialog } ) => ( + + ) } + /> + ) } + { isUploading && ( - - - { notice.message } - +
+ +
) } @@ -194,6 +209,18 @@ function LocalFonts() { supportedFormats ) } + { ! isUploading && notice && ( + + + + { notice.message } + + + ) }
); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js index 0ab4a7ba742247..2e7f413a6fa45b 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js @@ -7,7 +7,7 @@ */ import apiFetch from '@wordpress/api-fetch'; -export async function fetchInstallFonts( data ) { +export async function fetchInstallFont( data ) { const config = { path: '/wp/v2/font-families', method: 'POST', diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss index 86cac4244dea93..d026563d3b73ea 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/style.scss +++ b/packages/edit-site/src/components/global-styles/font-library-modal/style.scss @@ -24,7 +24,7 @@ } } -.font-library-modal__tab-layout { +.font-library-modal__tabpanel-layout { main { padding-bottom: 4rem; @@ -75,7 +75,7 @@ padding-bottom: 1rem; } -.font-library-modal__tab-panel { +.font-library-modal__tabs { [role="tablist"] { position: sticky; top: 0; @@ -94,6 +94,9 @@ justify-content: center; height: 250px; width: 100%; +} + +button.font-library-modal__upload-area { background-color: #f0f0f0; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/tab-layout.js b/packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js similarity index 85% rename from packages/edit-site/src/components/global-styles/font-library-modal/tab-layout.js rename to packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js index 07f27cd31ea79c..a7151c6e908d61 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/tab-layout.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/tab-panel-layout.js @@ -11,9 +11,15 @@ import { } from '@wordpress/components'; import { chevronLeft } from '@wordpress/icons'; -function TabLayout( { title, description, handleBack, children, footer } ) { +function TabPanelLayout( { + title, + description, + handleBack, + children, + footer, +} ) { return ( -
+
@@ -47,4 +53,4 @@ function TabLayout( { title, description, handleBack, children, footer } ) { ); } -export default TabLayout; +export default TabPanelLayout; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index 69db09d49a0cea..2874dd446efb45 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -130,16 +130,21 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) { return src; } -export function makeFormDataFromFontFamilies( fontFamilies ) { +export function makeFormDataFromFontFamily( fontFamily ) { const formData = new FormData(); - const newFontFamilies = fontFamilies.map( ( family, familyIndex ) => { - const { kebabCase } = unlock( componentsPrivateApis ); - family.slug = kebabCase( family.slug ); - if ( family?.fontFace ) { - family.fontFace = family.fontFace.map( ( face, faceIndex ) => { + const { kebabCase } = unlock( componentsPrivateApis ); + + const newFontFamily = { + ...fontFamily, + slug: kebabCase( fontFamily.slug ), + }; + + if ( newFontFamily?.fontFace ) { + const newFontFaces = newFontFamily.fontFace.map( + ( face, faceIndex ) => { if ( face.file ) { // Slugified file name because the it might contain spaces or characters treated differently on the server. - const fileId = `file-${ familyIndex }-${ faceIndex }`; + const fileId = `file-${ faceIndex }`; // Add the files to the formData formData.append( fileId, face.file, face.file.name ); // remove the file object from the face object the file is referenced by the uploadedFile key @@ -151,10 +156,11 @@ export function makeFormDataFromFontFamilies( fontFamilies ) { return newFace; } return face; - } ); - } - return family; - } ); - formData.append( 'font_families', JSON.stringify( newFontFamilies ) ); + } + ); + newFontFamily.fontFace = newFontFaces; + } + + formData.append( 'font_family_settings', JSON.stringify( newFontFamily ) ); return formData; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js deleted file mode 100644 index 4adae7889cc5e5..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamilies.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Internal dependencies - */ -import { makeFormDataFromFontFamilies } from '../index'; - -/* global File */ - -describe( 'makeFormDataFromFontFamilies', () => { - it( 'should process fontFamilies and return FormData', () => { - const mockFontFamilies = [ - { - slug: 'bebas', - name: 'Bebas', - fontFamily: 'Bebas', - fontFace: [ - { - file: new File( [ 'content' ], 'test-font1.woff2' ), - fontWeight: '500', - fontStyle: 'normal', - }, - { - file: new File( [ 'content' ], 'test-font2.woff2' ), - fontWeight: '400', - fontStyle: 'normal', - }, - ], - }, - ]; - - const formData = makeFormDataFromFontFamilies( mockFontFamilies ); - - expect( formData instanceof FormData ).toBeTruthy(); - - // Check if files are added correctly - expect( formData.get( 'file-0-0' ).name ).toBe( 'test-font1.woff2' ); - expect( formData.get( 'file-0-1' ).name ).toBe( 'test-font2.woff2' ); - - // Check if 'fontFamilies' key in FormData is correct - const expectedFontFamilies = [ - { - fontFace: [ - { - fontWeight: '500', - fontStyle: 'normal', - uploadedFile: 'file-0-0', - }, - { - fontWeight: '400', - fontStyle: 'normal', - uploadedFile: 'file-0-1', - }, - ], - slug: 'bebas', - name: 'Bebas', - fontFamily: 'Bebas', - }, - ]; - expect( JSON.parse( formData.get( 'font_families' ) ) ).toEqual( - expectedFontFamilies - ); - } ); -} ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js new file mode 100644 index 00000000000000..9f38903c89759b --- /dev/null +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/makeFormDataFromFontFamily.spec.js @@ -0,0 +1,58 @@ +/** + * Internal dependencies + */ +import { makeFormDataFromFontFamily } from '../index'; + +/* global File */ + +describe( 'makeFormDataFromFontFamily', () => { + it( 'should process fontFamilies and return FormData', () => { + const mockFontFamily = { + slug: 'bebas', + name: 'Bebas', + fontFamily: 'Bebas', + fontFace: [ + { + file: new File( [ 'content' ], 'test-font1.woff2' ), + fontWeight: '500', + fontStyle: 'normal', + }, + { + file: new File( [ 'content' ], 'test-font2.woff2' ), + fontWeight: '400', + fontStyle: 'normal', + }, + ], + }; + + const formData = makeFormDataFromFontFamily( mockFontFamily ); + + expect( formData instanceof FormData ).toBeTruthy(); + + // Check if files are added correctly + expect( formData.get( 'file-0' ).name ).toBe( 'test-font1.woff2' ); + expect( formData.get( 'file-1' ).name ).toBe( 'test-font2.woff2' ); + + // Check if 'fontFamilies' key in FormData is correct + const expectedFontFamily = { + fontFace: [ + { + fontWeight: '500', + fontStyle: 'normal', + uploadedFile: 'file-0', + }, + { + fontWeight: '400', + fontStyle: 'normal', + uploadedFile: 'file-1', + }, + ], + slug: 'bebas', + name: 'Bebas', + fontFamily: 'Bebas', + }; + expect( JSON.parse( formData.get( 'font_family_settings' ) ) ).toEqual( + expectedFontFamily + ); + } ); +} ); diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index db7a6877fef10e..dcdc98fefebb6f 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -6,45 +6,31 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { __, _n, sprintf } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { dateI18n, getDate, humanTimeDiff, getSettings } from '@wordpress/date'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; -import { useMemo } from '@wordpress/element'; -import { getBlockTypes } from '@wordpress/blocks'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ -import getRevisionChanges from './get-revision-changes'; +import { unlock } from '../../../lock-unlock'; const DAY_IN_MILLISECONDS = 60 * 60 * 1000 * 24; -const MAX_CHANGES = 7; +const { getGlobalStylesChanges } = unlock( blockEditorPrivateApis ); -function ChangesSummary( { revision, previousRevision, blockNames } ) { - const changes = getRevisionChanges( - revision, - previousRevision, - blockNames - ); +function ChangesSummary( { revision, previousRevision } ) { + const changes = getGlobalStylesChanges( revision, previousRevision, { + maxResults: 7, + } ); const changesLength = changes.length; if ( ! changesLength ) { return null; } - // Truncate to `n` results if necessary. - if ( changesLength > MAX_CHANGES ) { - const deleteCount = changesLength - MAX_CHANGES; - const andMoreText = sprintf( - // translators: %d: number of global styles changes that are not displayed in the UI. - _n( '…and %d more change.', '…and %d more changes.', deleteCount ), - deleteCount - ); - changes.splice( MAX_CHANGES, deleteCount, andMoreText ); - } - return ( { - const blockTypes = getBlockTypes(); - return blockTypes.reduce( ( accumulator, { name, title } ) => { - accumulator[ name ] = title; - return accumulator; - }, {} ); - }, [] ); const dateNowInMs = getDate().getTime(); const { datetimeAbbreviated } = getSettings().formats; @@ -219,7 +198,6 @@ function RevisionsButtons( { { isSelected && ( { - event.preventDefault(); -}; +const { DocumentTools: EditorDocumentTools } = unlock( editorPrivateApis ); export default function DocumentTools( { blockEditorMode, hasFixedToolbar, isDistractionFree, - showIconLabels, } ) { - const inserterButton = useRef(); - const { - isInserterOpen, - isListViewOpen, - listViewShortcut, - isVisualMode, - listViewToggleRef, - } = useSelect( ( select ) => { + const { isVisualMode } = useSelect( ( select ) => { const { getEditorMode } = select( editSiteStore ); - const { getShortcutRepresentation } = select( keyboardShortcutsStore ); - const { isInserterOpened, isListViewOpened, getListViewToggleRef } = - unlock( select( editorStore ) ); return { - isInserterOpen: isInserterOpened(), - isListViewOpen: isListViewOpened(), - listViewShortcut: getShortcutRepresentation( - 'core/editor/toggle-list-view' - ), isVisualMode: getEditorMode() === 'visual', - listViewToggleRef: getListViewToggleRef(), }; }, [] ); const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); - const { setDeviceType, setIsInserterOpened, setIsListViewOpened } = - useDispatch( editorStore ); - + const { setDeviceType } = useDispatch( editorStore ); 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 ] - ); - - // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. - const blockToolbarCanBeFocused = useCanBlockToolbarBeFocused(); - - /* 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 && ( + { isZoomedOutViewExperimentEnabled && + isLargeViewport && + ! isDistractionFree && + ! hasFixedToolbar && ( { + setDeviceType( 'Desktop' ); + __unstableSetEditorMode( + isZoomedOutView ? 'edit' : 'zoom-out' + ); + } } size="compact" /> ) } - { isLargeViewport && ( - <> - { ! hasFixedToolbar && ( - - ) } - - - { ! isDistractionFree && ( - - ) } - { isZoomedOutViewExperimentEnabled && - ! isDistractionFree && - ! hasFixedToolbar && ( - { - setDeviceType( 'Desktop' ); - __unstableSetEditorMode( - isZoomedOutView - ? 'edit' - : 'zoom-out' - ); - } } - size="compact" - /> - ) } - - ) } -
-
+ ); } 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 0d24865e74bf6a..773edf469143b9 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -66,21 +66,12 @@ export default function HeaderEditMode() { templateType: getEditedPostType(), blockEditorMode: __unstableGetEditorMode(), blockSelectionStart: getBlockSelectionStart(), - showIconLabels: getPreference( - editSiteStore.name, - 'showIconLabels' - ), + showIconLabels: getPreference( 'core', 'showIconLabels' ), editorCanvasView: unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), - hasFixedToolbar: getPreference( - editSiteStore.name, - 'fixedToolbar' - ), - isDistractionFree: getPreference( - editSiteStore.name, - 'distractionFree' - ), + hasFixedToolbar: getPreference( 'core', 'fixedToolbar' ), + isDistractionFree: getPreference( 'core', 'distractionFree' ), isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); @@ -136,7 +127,6 @@ export default function HeaderEditMode() { { isTopToolbar && ( <> @@ -209,14 +199,13 @@ export default function HeaderEditMode() { ) } >
) } - + { ! isDistractionFree && ( diff --git a/packages/edit-site/src/components/header-edit-mode/mode-switcher/index.js b/packages/edit-site/src/components/header-edit-mode/mode-switcher/index.js index f4b63edfae6e80..2c27444f669c03 100644 --- a/packages/edit-site/src/components/header-edit-mode/mode-switcher/index.js +++ b/packages/edit-site/src/components/header-edit-mode/mode-switcher/index.js @@ -33,10 +33,6 @@ function ModeSwitcher() { shortcut: select( keyboardShortcutsStore ).getShortcutRepresentation( 'core/edit-site/toggle-mode' ), - isRichEditingEnabled: - select( editSiteStore ).getSettings().richEditingEnabled, - isCodeEditingEnabled: - select( editSiteStore ).getSettings().codeEditingEnabled, mode: select( editSiteStore ).getEditorMode(), } ), [] diff --git a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js b/packages/edit-site/src/components/header-edit-mode/more-menu/index.js index 4d892461a48043..1c2991cced9e4b 100644 --- a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js +++ b/packages/edit-site/src/components/header-edit-mode/more-menu/index.js @@ -46,7 +46,7 @@ export default function MoreMenu( { showIconLabels } ) { const toggleDistractionFree = () => { registry.batch( () => { - setPreference( 'core/edit-site', 'fixedToolbar', true ); + setPreference( 'core', 'fixedToolbar', true ); setIsInserterOpened( false ); setIsListViewOpened( false ); closeGeneralSidebar(); @@ -54,7 +54,7 @@ export default function MoreMenu( { showIconLabels } ) { }; const turnOffDistractionFree = () => { - setPreference( 'core/edit-site', 'distractionFree', false ); + setPreference( 'core', 'distractionFree', false ); }; return ( @@ -69,7 +69,7 @@ export default function MoreMenu( { showIconLabels } ) { <> .components-button.has-icon, - .edit-site-header-edit-mode__toolbar > .components-dropdown > .components-button.has-icon { - // @todo: override toolbar group inherited paddings from components/block-tools/style.scss. - // This is best fixed by making the mover control area a proper single toolbar group. - // It needs specificity due to style inherited from .components-accessible-toolbar .components-button.has-icon.has-icon. - height: $button-size-compact; - min-width: $button-size-compact; - padding: 4px; - - &.is-pressed { - background: $gray-900; - } - - &:focus:not(:disabled) { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 0 $border-width $white; - outline: $border-width solid transparent; - } - - &::before { - display: none; - } - } - - .edit-site-header-edit-mode__toolbar > .edit-site-header-edit-mode__inserter-toggle.has-icon { - // Special dimensions for this button. - min-width: $button-size-compact; - width: $button-size-compact; - height: $button-size-compact; - padding: 0; - } - - .edit-site-header-edit-mode__toolbar > .edit-site-header-edit-mode__inserter-toggle.has-text.has-icon { - width: auto; - padding: 0 $grid-unit-10; - } -} - // Button text label styles .edit-site-header-edit-mode.show-icon-labels { @@ -185,10 +142,6 @@ $header-toolbar-min-width: 335px; padding: 0 $grid-unit-10; } - .edit-site-header-edit-mode__document-tools .edit-site-header-edit-mode__toolbar > * + * { - margin-left: $grid-unit-10; - } - .block-editor-block-mover { border-left: none; diff --git a/packages/edit-site/src/components/header-edit-mode/undo-redo/redo.js b/packages/edit-site/src/components/header-edit-mode/undo-redo/redo.js deleted file mode 100644 index 17d0ecb892d0a5..00000000000000 --- a/packages/edit-site/src/components/header-edit-mode/undo-redo/redo.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, isRTL } from '@wordpress/i18n'; -import { Button } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { redo as redoIcon, undo as undoIcon } from '@wordpress/icons'; -import { displayShortcut, isAppleOS } from '@wordpress/keycodes'; -import { store as coreStore } from '@wordpress/core-data'; -import { forwardRef } from '@wordpress/element'; - -function RedoButton( props, ref ) { - const shortcut = isAppleOS() - ? displayShortcut.primaryShift( 'z' ) - : displayShortcut.primary( 'y' ); - - const hasRedo = useSelect( - ( select ) => select( coreStore ).hasRedo(), - [] - ); - const { redo } = useDispatch( coreStore ); - return ( - + { showAddPageModal && ( + + ) } + + } > { view.type === LAYOUT_LIST && ( diff --git a/packages/edit-site/src/components/page-pages/style.scss b/packages/edit-site/src/components/page-pages/style.scss index 35ac8273dc555a..c2d2cc25529c4a 100644 --- a/packages/edit-site/src/components/page-pages/style.scss +++ b/packages/edit-site/src/components/page-pages/style.scss @@ -1,11 +1,6 @@ .edit-site-page-pages__featured-image { border-radius: $grid-unit-05; - width: $grid-unit-40; - height: $grid-unit-40; -} - - -.edit-site-page-pages__list-view-title-field { - font-size: $default-font-size; - font-weight: 500; + width: $grid-unit-50; + height: $grid-unit-50; + display: block; } diff --git a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js new file mode 100644 index 00000000000000..bf5210beb49fbf --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js @@ -0,0 +1,329 @@ +/** + * External dependencies + */ +import { paramCase as kebabCase } from 'change-case'; + +/** + * WordPress dependencies + */ +import { getQueryArgs } from '@wordpress/url'; +import { downloadBlob } from '@wordpress/blob'; +import { __, _x, sprintf } from '@wordpress/i18n'; +import { + Button, + TextControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + __experimentalText as Text, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; +import { decodeEntities } from '@wordpress/html-entities'; +import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { store as editSiteStore } from '../../store'; +import { + PATTERN_TYPES, + TEMPLATE_PART_POST_TYPE, + PATTERN_DEFAULT_CATEGORY, +} from '../../utils/constants'; +import { CreateTemplatePartModalContents } from '../create-template-part-modal'; + +const { useHistory } = unlock( routerPrivateApis ); +const { CreatePatternModalContents, useDuplicatePatternProps } = + unlock( patternsPrivateApis ); + +export const exportJSONaction = { + id: 'export-pattern', + label: __( 'Export as JSON' ), + isEligible: ( item ) => item.type === PATTERN_TYPES.user, + callback: ( item ) => { + const json = { + __file: item.type, + title: item.title || item.name, + content: item.patternPost.content.raw, + syncStatus: item.patternPost.wp_pattern_sync_status, + }; + return downloadBlob( + `${ kebabCase( item.title || item.name ) }.json`, + JSON.stringify( json, null, 2 ), + 'application/json' + ); + }, +}; + +export const renameAction = { + id: 'rename-pattern', + label: __( 'Rename' ), + isEligible: ( item ) => { + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + const isUserPattern = item.type === PATTERN_TYPES.user; + const isCustomPattern = + isUserPattern || ( isTemplatePart && item.isCustom ); + const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file; + return isCustomPattern && ! hasThemeFile; + }, + RenderModal: ( { item, closeModal } ) => { + const [ title, setTitle ] = useState( () => item.title ); + const { editEntityRecord, saveEditedEntityRecord } = + useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + async function onRename( event ) { + event.preventDefault(); + try { + await editEntityRecord( 'postType', item.type, item.id, { + title, + } ); + // Update state before saving rerenders the list. + setTitle( '' ); + closeModal(); + // Persist edited entity. + await saveEditedEntityRecord( 'postType', item.type, item.id, { + throwOnError: true, + } ); + createSuccessNotice( + item.type === TEMPLATE_PART_POST_TYPE + ? __( 'Template part renamed.' ) + : __( 'Pattern renamed.' ), + { type: 'snackbar' } + ); + } catch ( error ) { + const fallbackErrorMessage = + item.type === TEMPLATE_PART_POST_TYPE + ? __( + 'An error occurred while renaming the template part.' + ) + : __( 'An error occurred while renaming the pattern.' ); + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : fallbackErrorMessage; + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + } + return ( +
+ + + + + + + +
+ ); + }, +}; + +const canDeleteOrReset = ( item ) => { + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + const isUserPattern = item.type === PATTERN_TYPES.user; + return isUserPattern || ( isTemplatePart && item.isCustom ); +}; + +export const deleteAction = { + id: 'delete-pattern', + label: __( 'Delete' ), + isEligible: ( item ) => { + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file; + return canDeleteOrReset( item ) && ! hasThemeFile; + }, + hideModalHeader: true, + RenderModal: ( { item, closeModal } ) => { + const { __experimentalDeleteReusableBlock } = + useDispatch( reusableBlocksStore ); + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + const { removeTemplate } = useDispatch( editSiteStore ); + + const deletePattern = async () => { + try { + await __experimentalDeleteReusableBlock( item.id ); + createSuccessNotice( + sprintf( + // translators: %s: The pattern's title e.g. 'Call to action'. + __( '"%s" deleted.' ), + item.title + ), + { type: 'snackbar', id: 'edit-site-patterns-success' } + ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while deleting the pattern.' ); + createErrorNotice( errorMessage, { + type: 'snackbar', + id: 'edit-site-patterns-error', + } ); + } + }; + const deleteItem = () => + item.type === TEMPLATE_PART_POST_TYPE + ? removeTemplate( item ) + : deletePattern(); + return ( + + + { sprintf( + // translators: %s: The pattern or template part's title e.g. 'Call to action'. + __( 'Are you sure you want to delete "%s"?' ), + decodeEntities( item.title || item.name ) + ) } + + + + + + + ); + }, +}; + +export const resetAction = { + id: 'reset-action', + label: __( 'Clear customizations' ), + isEligible: ( item ) => { + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file; + return canDeleteOrReset( item ) && hasThemeFile; + }, + hideModalHeader: true, + RenderModal: ( { item, closeModal } ) => { + const { removeTemplate } = useDispatch( editSiteStore ); + return ( + + + { __( + 'Are you sure you want to clear these customizations?' + ) } + + + + + + + ); + }, +}; + +export const duplicatePatternAction = { + id: 'duplicate-pattern', + label: _x( 'Duplicate', 'action label' ), + isEligible: ( item ) => item.type !== TEMPLATE_PART_POST_TYPE, + modalHeader: _x( 'Duplicate pattern', 'action label' ), + RenderModal: ( { item, closeModal } ) => { + const { categoryId = PATTERN_DEFAULT_CATEGORY } = getQueryArgs( + window.location.href + ); + const isThemePattern = item.type === PATTERN_TYPES.theme; + const history = useHistory(); + function onPatternSuccess( { pattern } ) { + history.push( { + categoryType: PATTERN_TYPES.theme, + categoryId, + postType: PATTERN_TYPES.user, + postId: pattern.id, + } ); + closeModal(); + } + const duplicatedProps = useDuplicatePatternProps( { + pattern: isThemePattern ? item : item.patternPost, + onSuccess: onPatternSuccess, + } ); + return ( + + ); + }, +}; + +export const duplicateTemplatePartAction = { + id: 'duplicate-template-part', + label: _x( 'Duplicate', 'action label' ), + isEligible: ( item ) => item.type === TEMPLATE_PART_POST_TYPE, + modalHeader: _x( 'Duplicate template part', 'action label' ), + RenderModal: ( { item, closeModal } ) => { + const { createSuccessNotice } = useDispatch( noticesStore ); + const { categoryId = PATTERN_DEFAULT_CATEGORY } = getQueryArgs( + window.location.href + ); + const history = useHistory(); + async function onTemplatePartSuccess( templatePart ) { + createSuccessNotice( + sprintf( + // translators: %s: The new template part's title e.g. 'Call to action (copy)'. + __( '"%s" duplicated.' ), + item.title + ), + { type: 'snackbar', id: 'edit-site-patterns-success' } + ); + history.push( { + postType: TEMPLATE_PART_POST_TYPE, + postId: templatePart?.id, + categoryType: TEMPLATE_PART_POST_TYPE, + categoryId, + } ); + closeModal(); + } + return ( + + ); + }, +}; diff --git a/packages/edit-site/src/components/page-patterns/dataviews-patterns.js b/packages/edit-site/src/components/page-patterns/dataviews-patterns.js new file mode 100644 index 00000000000000..ad474d882cfcf6 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/dataviews-patterns.js @@ -0,0 +1,380 @@ +/** + * WordPress dependencies + */ +import { + __experimentalHStack as HStack, + Button, + __experimentalHeading as Heading, + Tooltip, + Flex, +} from '@wordpress/components'; +import { getQueryArgs } from '@wordpress/url'; +import { __, _x } from '@wordpress/i18n'; +import { + useState, + useMemo, + useCallback, + useId, + useEffect, +} from '@wordpress/element'; +import { + BlockPreview, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { + DataViews, + sortByTextFields, + getPaginationResults, +} from '@wordpress/dataviews'; +import { + Icon, + header, + footer, + symbolFilled as uncategorized, + symbol, + lockSmall, +} from '@wordpress/icons'; +import { usePrevious } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import Page from '../page'; +import { + LAYOUT_GRID, + PATTERN_TYPES, + TEMPLATE_PART_POST_TYPE, + PATTERN_SYNC_TYPES, + PATTERN_DEFAULT_CATEGORY, + ENUMERATION_TYPE, + OPERATOR_IN, +} from '../../utils/constants'; +import { + exportJSONaction, + renameAction, + resetAction, + deleteAction, + duplicatePatternAction, + duplicateTemplatePartAction, +} from './dataviews-pattern-actions'; +import usePatternSettings from './use-pattern-settings'; +import { unlock } from '../../lock-unlock'; +import usePatterns from './use-patterns'; +import PatternsHeader from './header'; +import { useLink } from '../routes/link'; + +const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( + blockEditorPrivateApis +); + +const templatePartIcons = { header, footer, uncategorized }; +const EMPTY_ARRAY = []; +const defaultConfigPerViewType = { + [ LAYOUT_GRID ]: { + mediaField: 'preview', + primaryField: 'title', + }, +}; +const DEFAULT_VIEW = { + type: LAYOUT_GRID, + search: '', + page: 1, + perPage: 20, + hiddenFields: [ 'sync-status' ], + layout: { + ...defaultConfigPerViewType[ LAYOUT_GRID ], + }, + filters: [], +}; + +const SYNC_FILTERS = [ + { + value: PATTERN_SYNC_TYPES.full, + label: _x( 'Synced', 'Option that shows all synchronized patterns' ), + description: __( 'Patterns that are kept in sync across the site.' ), + }, + { + value: PATTERN_SYNC_TYPES.unsynced, + label: _x( + 'Not synced', + 'Option that shows all patterns that are not synchronized' + ), + description: __( + 'Patterns that can be changed freely without affecting the site.' + ), + }, +]; + +function Preview( { item, viewType } ) { + const descriptionId = useId(); + const isUserPattern = item.type === PATTERN_TYPES.user; + const isNonUserPattern = item.type === PATTERN_TYPES.theme; + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + const isEmpty = ! item.blocks?.length; + // Only custom patterns or custom template parts can be renamed or deleted. + const isCustomPattern = + isUserPattern || ( isTemplatePart && item.isCustom ); + const ariaDescriptions = []; + if ( isCustomPattern ) { + // User patterns don't have descriptions, but can be edited and deleted, so include some help text. + ariaDescriptions.push( + __( 'Press Enter to edit, or Delete to delete the pattern.' ) + ); + } else if ( item.description ) { + ariaDescriptions.push( item.description ); + } + + if ( isNonUserPattern ) { + ariaDescriptions.push( + __( 'Theme & plugin patterns cannot be edited.' ) + ); + } + const [ backgroundColor ] = useGlobalStyle( 'color.background' ); + return ( + <> +
+ { isEmpty && isTemplatePart && __( 'Empty template part' ) } + { isEmpty && ! isTemplatePart && __( 'Empty pattern' ) } + { ! isEmpty && } +
+ { ariaDescriptions.map( ( ariaDescription, index ) => ( + + ) ) } + + ); +} + +function Title( { item, categoryId } ) { + const isUserPattern = item.type === PATTERN_TYPES.user; + const isNonUserPattern = item.type === PATTERN_TYPES.theme; + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + let itemIcon; + const { onClick } = useLink( { + postType: item.type, + postId: isUserPattern ? item.id : item.name, + categoryId, + categoryType: isTemplatePart ? item.type : PATTERN_TYPES.theme, + } ); + if ( ! isUserPattern && templatePartIcons[ categoryId ] ) { + itemIcon = templatePartIcons[ categoryId ]; + } else { + itemIcon = + item.syncStatus === PATTERN_SYNC_TYPES.full ? symbol : undefined; + } + return ( + + { itemIcon && ! isNonUserPattern && ( + + + + ) } + { item.type === PATTERN_TYPES.theme && ( + + + + ) } + + { item.type === PATTERN_TYPES.theme ? ( + + { item.title } + + ) : ( + + + + ) } + + + ); +} + +export default function DataviewsPatterns() { + const { categoryType, categoryId = PATTERN_DEFAULT_CATEGORY } = + getQueryArgs( window.location.href ); + const type = categoryType || PATTERN_TYPES.theme; + const [ view, setView ] = useState( DEFAULT_VIEW ); + const isUncategorizedThemePatterns = + type === PATTERN_TYPES.theme && categoryId === 'uncategorized'; + const previousCategoryId = usePrevious( categoryId ); + const viewSyncStatus = view.filters?.find( + ( { field } ) => field === 'sync-status' + )?.value; + const { patterns, isResolving } = usePatterns( + type, + isUncategorizedThemePatterns ? '' : categoryId, + { + search: view.search, + syncStatus: viewSyncStatus, + } + ); + const fields = useMemo( () => { + const _fields = [ + { + header: __( 'Preview' ), + id: 'preview', + render: ( { item } ) => ( + + ), + enableSorting: false, + enableHiding: false, + }, + { + header: __( 'Title' ), + id: 'title', + getValue: ( { item } ) => item.title, + render: ( { item } ) => ( + + ), + enableHiding: false, + }, + ]; + if ( type === PATTERN_TYPES.theme ) { + _fields.push( { + header: __( 'Sync Status' ), + id: 'sync-status', + render: ( { item } ) => { + // User patterns can have their sync statuses checked directly. + // Non-user patterns are all unsynced for the time being. + return ( + SYNC_FILTERS.find( + ( { value } ) => value === item.syncStatus + )?.label || + SYNC_FILTERS.find( + ( { value } ) => + value === PATTERN_SYNC_TYPES.unsynced + ).label + ); + }, + type: ENUMERATION_TYPE, + elements: SYNC_FILTERS, + filterBy: { + operators: [ OPERATOR_IN ], + }, + enableSorting: false, + } ); + } + return _fields; + }, [ view.type, categoryId, type ] ); + // Reset the page number when the category changes. + useEffect( () => { + if ( previousCategoryId !== categoryId ) { + setView( DEFAULT_VIEW ); + } + }, [ categoryId, previousCategoryId ] ); + const { data, paginationInfo } = useMemo( () => { + if ( ! patterns ) { + return { + data: EMPTY_ARRAY, + paginationInfo: { totalItems: 0, totalPages: 0 }, + }; + } + let filteredData = [ ...patterns ]; + // Handle sorting. + if ( view.sort ) { + filteredData = sortByTextFields( { + data: filteredData, + view, + fields, + textFields: [ 'title', 'author' ], + } ); + } + // Handle pagination. + return getPaginationResults( { + data: filteredData, + view, + } ); + }, [ patterns, view, fields ] ); + + const actions = useMemo( + () => [ + renameAction, + duplicatePatternAction, + duplicateTemplatePartAction, + exportJSONaction, + resetAction, + deleteAction, + ], + [] + ); + const onChangeView = useCallback( + ( newView ) => { + if ( newView.type !== view.type ) { + newView = { + ...newView, + layout: { + ...defaultConfigPerViewType[ newView.type ], + }, + }; + } + setView( newView ); + }, + [ view.type, setView ] + ); + const id = useId(); + const settings = usePatternSettings(); + // Wrap everything in a block editor provider. + // This ensures 'styles' that are needed for the previews are synced + // from the site editor store to the block editor store. + // TODO: check if I add the provider in every preview like in templates... + return ( + <ExperimentalBlockEditorProvider settings={ settings }> + <Page + title={ __( 'Patterns content' ) } + className="edit-site-page-patterns-dataviews" + hideTitleFromUI + > + <PatternsHeader + categoryId={ categoryId } + type={ type } + titleId={ `${ id }-title` } + descriptionId={ `${ id }-description` } + /> + <DataViews + paginationInfo={ paginationInfo } + fields={ fields } + actions={ actions } + data={ data || EMPTY_ARRAY } + getItemId={ ( item ) => item.name } + isLoading={ isResolving } + view={ view } + onChangeView={ onChangeView } + deferredRendering={ true } + supportedLayouts={ [ LAYOUT_GRID ] } + /> + </Page> + </ExperimentalBlockEditorProvider> + ); +} diff --git a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js index 118c954a851f3f..e82666902ed16a 100644 --- a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js +++ b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js @@ -81,7 +81,7 @@ export default function DuplicateMenuItem( { <DuplicatePatternModal onClose={ closeModal } onSuccess={ onPatternSuccess } - pattern={ isThemePattern ? item : item.patternBlock } + pattern={ isThemePattern ? item : item.patternPost } /> ) } { isModalOpen && isTemplatePart && ( 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 bacb0f31908635..8d2cbaf7806b4d 100644 --- a/packages/edit-site/src/components/page-patterns/grid-item.js +++ b/packages/edit-site/src/components/page-patterns/grid-item.js @@ -114,8 +114,8 @@ function GridItem( { categoryId, item, ...props } ) { const json = { __file: item.type, title: item.title || item.name, - content: item.patternBlock.content.raw, - syncStatus: item.patternBlock.wp_pattern_sync_status, + content: item.patternPost.content.raw, + syncStatus: item.patternPost.wp_pattern_sync_status, }; return downloadBlob( diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index 8995a0d25c96cf..dd3c52ef08c1a3 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -223,3 +223,69 @@ } } } + +/** + * DataViews patterns styles. + * TODO: when this becomes stable, consolidate styles with the above. + */ +.edit-site-page-patterns-dataviews { + .page-patterns-preview-field { + &.is-viewtype-grid { + .block-editor-block-preview__container { + height: auto; + } + } + } + + .edit-site-patterns__pattern-icon { + fill: var(--wp-block-synced-color); + flex-shrink: 0; + } + + .edit-site-patterns__pattern-lock-icon { + min-width: min-content; + } + + .edit-site-patterns__section-header { + border-bottom: 1px solid #f0f0f0; + min-height: 72px; + padding: $grid-unit-20 $grid-unit-40; + position: sticky; + top: 0; + z-index: 2; + } +} + +.dataviews-action-modal__duplicate-pattern { + // Fix the modal width to prevent added categories from stretching the modal. + [role="dialog"] > [role="document"] { + width: 350px; + } + + .patterns-menu-items__convert-modal-categories { + position: relative; + } + + .components-form-token-field__suggestions-list:not(:empty) { + position: absolute; + border: $border-width solid var(--wp-admin-theme-color); + border-bottom-left-radius: $radius-block-ui; + border-bottom-right-radius: $radius-block-ui; + box-shadow: 0 0 0.5px 0.5px var(--wp-admin-theme-color); + box-sizing: border-box; + z-index: 1; + background-color: $white; + width: calc(100% + 2px); // Account for the border width of the token field. + left: -1px; + min-width: initial; + max-height: $grid-unit-60 * 2; // Adjust to not cover the save button, showing three items. + } +} + +.dataviews-action-modal__duplicate-template-part { + .components-modal__frame { + @include break-small { + max-width: 500px; + } + } +} 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 be5992bd9b4efe..a0b82247c85a6d 100644 --- a/packages/edit-site/src/components/page-patterns/use-patterns.js +++ b/packages/edit-site/src/components/page-patterns/use-patterns.js @@ -184,29 +184,38 @@ const selectPatterns = createSelector( ] ); -const patternBlockToPattern = ( patternBlock, categories ) => ( { - blocks: parse( patternBlock.content.raw, { +/** + * Converts a post of type `wp_block` to a 'pattern item' that more closely + * matches the structure of theme provided patterns. + * + * @param {Object} patternPost The `wp_block` record being normalized. + * @param {Map} categories A Map of user created categories. + * + * @return {Object} The normalized item. + */ +const convertPatternPostToItem = ( patternPost, categories ) => ( { + blocks: parse( patternPost.content.raw, { __unstableSkipMigrationLogs: true, } ), - ...( patternBlock.wp_pattern_category.length > 0 && { - categories: patternBlock.wp_pattern_category.map( + ...( patternPost.wp_pattern_category.length > 0 && { + categories: patternPost.wp_pattern_category.map( ( patternCategoryId ) => categories && categories.get( patternCategoryId ) ? categories.get( patternCategoryId ).slug : patternCategoryId ), } ), - termLabels: patternBlock.wp_pattern_category.map( ( patternCategoryId ) => + termLabels: patternPost.wp_pattern_category.map( ( patternCategoryId ) => categories?.get( patternCategoryId ) ? categories.get( patternCategoryId ).label : patternCategoryId ), - id: patternBlock.id, - name: patternBlock.slug, - syncStatus: patternBlock.wp_pattern_sync_status || PATTERN_SYNC_TYPES.full, - title: patternBlock.title.raw, - type: PATTERN_TYPES.user, - patternBlock, + id: patternPost.id, + name: patternPost.slug, + syncStatus: patternPost.wp_pattern_sync_status || PATTERN_SYNC_TYPES.full, + title: patternPost.title.raw, + type: patternPost.type, + patternPost, } ); const selectUserPatterns = createSelector( @@ -215,7 +224,7 @@ const selectUserPatterns = createSelector( select( coreStore ); const query = { per_page: -1 }; - const records = getEntityRecords( + const patternPosts = getEntityRecords( 'postType', PATTERN_TYPES.user, query @@ -225,9 +234,9 @@ const selectUserPatterns = createSelector( userPatternCategories.forEach( ( userCategory ) => categories.set( userCategory.id, userCategory ) ); - let patterns = records - ? records.map( ( record ) => - patternBlockToPattern( record, categories ) + let patterns = patternPosts + ? patternPosts.map( ( record ) => + convertPatternPostToItem( record, categories ) ) : EMPTY_PATTERN_LIST; diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 9c52aaa7f12f63..c0e0289311db6a 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -24,7 +24,11 @@ import { BlockPreview, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { DataViews } from '@wordpress/dataviews'; +import { + DataViews, + sortByTextFields, + getPaginationResults, +} from '@wordpress/dataviews'; import { privateApis as routerPrivateApis } from '@wordpress/router'; /** @@ -88,19 +92,18 @@ function normalizeSearchInput( input = '' ) { return removeAccents( input.trim().toLowerCase() ); } -function TemplateTitle( { item, view } ) { - if ( view.type === LAYOUT_LIST ) { +function TemplateTitle( { item, viewType } ) { + if ( viewType === LAYOUT_LIST ) { return ( <> - { decodeEntities( item.title?.rendered || item.slug ) || - __( '(no title)' ) } + { decodeEntities( item.title?.rendered ) || __( '(no title)' ) } </> ); } return ( <VStack spacing={ 1 }> - <View as="span" className="edit-site-list-title__customized-info"> + <View as="span" className="dataviews-view-grid__title-field"> <Link params={ { postId: item.id, @@ -108,7 +111,7 @@ function TemplateTitle( { item, view } ) { canvas: 'edit', } } > - { decodeEntities( item.title?.rendered || item.slug ) || + { decodeEntities( item.title?.rendered ) || __( '(no title)' ) } </Link> </View> @@ -116,9 +119,9 @@ function TemplateTitle( { item, view } ) { ); } -function AuthorField( { item, view } ) { +function AuthorField( { item, viewType } ) { const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); - const withIcon = view.type !== LAYOUT_LIST; + const withIcon = viewType !== LAYOUT_LIST; return ( <HStack alignment="left" spacing={ 1 }> @@ -210,9 +213,9 @@ export default function DataviewsTemplates() { { header: __( 'Template' ), id: 'title', - getValue: ( { item } ) => item.title?.rendered || item.slug, + getValue: ( { item } ) => item.title?.rendered, render: ( { item } ) => ( - <TemplateTitle item={ item } view={ view } /> + <TemplateTitle item={ item } viewType={ view.type } /> ), maxWidth: 400, enableHiding: false, @@ -243,20 +246,20 @@ export default function DataviewsTemplates() { id: 'author', getValue: ( { item } ) => item.author_text, render: ( { item } ) => { - return <AuthorField view={ view } item={ item } />; + return <AuthorField viewType={ view.type } item={ item } />; }, enableHiding: false, type: ENUMERATION_TYPE, elements: authors, }, ], - [ authors, view ] + [ authors, view.type ] ); - const { shownTemplates, paginationInfo } = useMemo( () => { + const { data, paginationInfo } = useMemo( () => { if ( ! allTemplates ) { return { - shownTemplates: EMPTY_ARRAY, + data: EMPTY_ARRAY, paginationInfo: { totalItems: 0, totalPages: 0 }, }; } @@ -302,36 +305,18 @@ export default function DataviewsTemplates() { // Handle sorting. if ( view.sort ) { - const stringSortingFields = [ 'title', 'author' ]; - const fieldId = view.sort.field; - if ( stringSortingFields.includes( fieldId ) ) { - const fieldToSort = fields.find( ( field ) => { - return field.id === fieldId; - } ); - filteredTemplates.sort( ( a, b ) => { - const valueA = fieldToSort.getValue( { item: a } ) ?? ''; - const valueB = fieldToSort.getValue( { item: b } ) ?? ''; - return view.sort.direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); - } ); - } + filteredTemplates = sortByTextFields( { + data: filteredTemplates, + view, + fields, + textFields: [ 'title' ], + } ); } - // 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 ), - }, - }; + return getPaginationResults( { + data: filteredTemplates, + view, + } ); }, [ allTemplates, view, fields ] ); const resetTemplateAction = useResetTemplateAction(); @@ -382,7 +367,7 @@ export default function DataviewsTemplates() { paginationInfo={ paginationInfo } fields={ fields } actions={ actions } - data={ shownTemplates } + data={ data } isLoading={ isLoadingData } view={ view } onChangeView={ onChangeView } diff --git a/packages/edit-site/src/components/preferences-modal/enable-feature.js b/packages/edit-site/src/components/preferences-modal/enable-feature.js index 9cd2105ba69fff..efda1c67352fcd 100644 --- a/packages/edit-site/src/components/preferences-modal/enable-feature.js +++ b/packages/edit-site/src/components/preferences-modal/enable-feature.js @@ -6,16 +6,20 @@ import { ___unstablePreferencesModalBaseOption as BaseOption } from '@wordpress/ import { store as preferencesStore } from '@wordpress/preferences'; export default function EnableFeature( props ) { - const { featureName, onToggle = () => {}, ...remainingProps } = props; + const { + scope = 'core/edit-site', + featureName, + onToggle = () => {}, + ...remainingProps + } = props; const isChecked = useSelect( - ( select ) => - !! select( preferencesStore ).get( 'core/edit-site', featureName ), - [ featureName ] + ( select ) => !! select( preferencesStore ).get( scope, featureName ), + [ scope, featureName ] ); const { toggle } = useDispatch( preferencesStore ); const onChange = () => { onToggle(); - toggle( 'core/edit-site', featureName ); + toggle( scope, featureName ); }; return ( <BaseOption diff --git a/packages/edit-site/src/components/preferences-modal/enable-panel-option.js b/packages/edit-site/src/components/preferences-modal/enable-panel-option.js new file mode 100644 index 00000000000000..6c9ea22b7f17dd --- /dev/null +++ b/packages/edit-site/src/components/preferences-modal/enable-panel-option.js @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { compose, ifCondition } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { ___unstablePreferencesModalBaseOption as BaseOption } from '@wordpress/interface'; +import { store as editorStore } from '@wordpress/editor'; + +export default compose( + withSelect( ( select, { panelName } ) => { + const { isEditorPanelEnabled, isEditorPanelRemoved } = + select( editorStore ); + return { + isRemoved: isEditorPanelRemoved( panelName ), + isChecked: isEditorPanelEnabled( panelName ), + }; + } ), + ifCondition( ( { isRemoved } ) => ! isRemoved ), + withDispatch( ( dispatch, { panelName } ) => ( { + onChange: () => + dispatch( editorStore ).toggleEditorPanelEnabled( panelName ), + } ) ) +)( BaseOption ); diff --git a/packages/edit-site/src/components/preferences-modal/index.js b/packages/edit-site/src/components/preferences-modal/index.js index 64eb06b6530ca0..8634e3e18b23ed 100644 --- a/packages/edit-site/src/components/preferences-modal/index.js +++ b/packages/edit-site/src/components/preferences-modal/index.js @@ -7,16 +7,23 @@ import { PreferencesModalSection, store as interfaceStore, } from '@wordpress/interface'; -import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; import { store as preferencesStore } from '@wordpress/preferences'; -import { store as editorStore } from '@wordpress/editor'; +import { + PostTaxonomies, + PostExcerptCheck, + PageAttributesCheck, + PostFeaturedImageCheck, + PostTypeSupportCheck, + store as editorStore, +} from '@wordpress/editor'; /** * Internal dependencies */ import EnableFeature from './enable-feature'; +import EnablePanelOption from './enable-panel-option'; import { store as editSiteStore } from '../../store'; export const PREFERENCES_MODAL_NAME = 'edit-site/preferences'; @@ -36,7 +43,7 @@ export default function EditSitePreferencesModal() { const { set: setPreference } = useDispatch( preferencesStore ); const toggleDistractionFree = () => { registry.batch( () => { - setPreference( 'core/edit-site', 'fixedToolbar', true ); + setPreference( 'core', 'fixedToolbar', true ); setIsInserterOpened( false ); setIsListViewOpened( false ); closeGeneralSidebar(); @@ -44,37 +51,83 @@ export default function EditSitePreferencesModal() { }; const turnOffDistractionFree = () => { - setPreference( 'core/edit-site', 'distractionFree', false ); + setPreference( 'core', 'distractionFree', false ); }; - const sections = useMemo( () => [ + const sections = [ { name: 'general', tabLabel: __( 'General' ), content: ( - <PreferencesModalSection title={ __( 'Interface' ) }> - <EnableFeature - featureName="showListViewByDefault" - help={ __( - 'Opens the block list view sidebar by default.' - ) } - label={ __( 'Always open list view' ) } - /> - <EnableFeature - featureName="showBlockBreadcrumbs" - help={ __( - 'Shows block breadcrumbs at the bottom of the editor.' - ) } - label={ __( 'Display block breadcrumbs' ) } - /> - <EnableFeature - featureName="allowRightClickOverrides" - help={ __( - 'Allows contextual list view menus via right-click, overriding browser defaults.' + <> + <PreferencesModalSection title={ __( 'Interface' ) }> + <EnableFeature + scope="core" + featureName="showListViewByDefault" + help={ __( + 'Opens the block list view sidebar by default.' + ) } + label={ __( 'Always open list view' ) } + /> + <EnableFeature + scope="core" + featureName="showBlockBreadcrumbs" + help={ __( + 'Shows block breadcrumbs at the bottom of the editor.' + ) } + label={ __( 'Display block breadcrumbs' ) } + /> + <EnableFeature + scope="core" + featureName="allowRightClickOverrides" + help={ __( + 'Allows contextual list view menus via right-click, overriding browser defaults.' + ) } + label={ __( 'Allow right-click contextual menus' ) } + /> + </PreferencesModalSection> + <PreferencesModalSection + title={ __( 'Document settings' ) } + description={ __( + 'Select what settings are shown in the document panel.' ) } - label={ __( 'Allow right-click contextual menus' ) } - /> - </PreferencesModalSection> + > + <PostTaxonomies + taxonomyWrapper={ ( content, taxonomy ) => ( + <EnablePanelOption + label={ taxonomy.labels.menu_name } + panelName={ `taxonomy-panel-${ taxonomy.slug }` } + /> + ) } + /> + <PostFeaturedImageCheck> + <EnablePanelOption + label={ __( 'Featured image' ) } + panelName="featured-image" + /> + </PostFeaturedImageCheck> + <PostExcerptCheck> + <EnablePanelOption + label={ __( 'Excerpt' ) } + panelName="post-excerpt" + /> + </PostExcerptCheck> + <PostTypeSupportCheck + supportKeys={ [ 'comments', 'trackbacks' ] } + > + <EnablePanelOption + label={ __( 'Discussion' ) } + panelName="discussion-panel" + /> + </PostTypeSupportCheck> + <PageAttributesCheck> + <EnablePanelOption + label={ __( 'Page attributes' ) } + panelName="page-attributes" + /> + </PageAttributesCheck> + </PreferencesModalSection> + </> ), }, { @@ -88,6 +141,7 @@ export default function EditSitePreferencesModal() { ) } > <EnableFeature + scope="core" featureName="fixedToolbar" onToggle={ turnOffDistractionFree } help={ __( @@ -96,6 +150,7 @@ export default function EditSitePreferencesModal() { label={ __( 'Top toolbar' ) } /> <EnableFeature + scope="core" featureName="distractionFree" onToggle={ toggleDistractionFree } help={ __( @@ -104,6 +159,7 @@ export default function EditSitePreferencesModal() { label={ __( 'Distraction free' ) } /> <EnableFeature + scope="core" featureName="focusMode" help={ __( 'Highlights the current block and fades other content.' @@ -125,6 +181,7 @@ export default function EditSitePreferencesModal() { ) } > <EnableFeature + scope="core" featureName="keepCaretInsideBlock" help={ __( 'Keeps the text cursor within the block boundaries, aiding users with screen readers by preventing unintentional cursor movement outside the block.' @@ -134,6 +191,7 @@ export default function EditSitePreferencesModal() { </PreferencesModalSection> <PreferencesModalSection title={ __( 'Interface' ) }> <EnableFeature + scope="core" featureName="showIconLabels" label={ __( 'Show button text labels' ) } help={ __( @@ -144,7 +202,25 @@ export default function EditSitePreferencesModal() { </> ), }, - ] ); + { + name: 'blocks', + tabLabel: __( 'Blocks' ), + content: ( + <> + <PreferencesModalSection title={ __( 'Inserter' ) }> + <EnableFeature + scope="core" + featureName="mostUsedBlocks" + help={ __( + 'Adds a category with the most frequently used blocks in the inserter.' + ) } + label={ __( 'Show most used blocks' ) } + /> + </PreferencesModalSection> + </> + ), + }, + ]; if ( ! isModalActive ) { return null; } diff --git a/packages/edit-site/src/components/routes/link.js b/packages/edit-site/src/components/routes/link.js index 3191e6b9c6f3ac..9ee60b5ef8b9e8 100644 --- a/packages/edit-site/src/components/routes/link.js +++ b/packages/edit-site/src/components/routes/link.js @@ -15,9 +15,12 @@ import { const { useHistory } = unlock( routerPrivateApis ); -export function useLink( params = {}, state, shouldReplace = false ) { - const history = useHistory(); - +export function getPostLinkProps( + history, + params = {}, + state, + shouldReplace = false +) { function onClick( event ) { event.preventDefault(); @@ -49,6 +52,11 @@ export function useLink( params = {}, state, shouldReplace = false ) { }; } +export function useLink( params, state, shouldReplace ) { + const history = useHistory(); + return getPostLinkProps( history, params, state, shouldReplace ); +} + export default function Link( { params = {}, state, diff --git a/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js deleted file mode 100644 index 9924a1471ae2f1..00000000000000 --- a/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { Button, VisuallyHidden } from '@wordpress/components'; -import { __experimentalLibrary as Library } from '@wordpress/block-editor'; -import { close } from '@wordpress/icons'; -import { - useViewportMatch, - __experimentalUseDialog as useDialog, -} from '@wordpress/compose'; -import { __ } from '@wordpress/i18n'; -import { useEffect, useRef } from '@wordpress/element'; -import { store as editorStore } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; - -export default function InserterSidebar() { - const { setIsInserterOpened } = useDispatch( editorStore ); - const insertionPoint = useSelect( - ( select ) => unlock( select( editorStore ) ).getInsertionPoint(), - [] - ); - - const isMobile = useViewportMatch( 'medium', '<' ); - const TagName = ! isMobile ? VisuallyHidden : 'div'; - const [ inserterDialogRef, inserterDialogProps ] = useDialog( { - onClose: () => setIsInserterOpened( false ), - focusOnMount: null, - } ); - - const libraryRef = useRef(); - useEffect( () => { - libraryRef.current.focusSearch(); - }, [] ); - - return ( - <div - ref={ inserterDialogRef } - { ...inserterDialogProps } - className="edit-site-editor__inserter-panel" - > - <TagName className="edit-site-editor__inserter-panel-header"> - <Button - icon={ close } - label={ __( 'Close block inserter' ) } - onClick={ () => setIsInserterOpened( false ) } - /> - </TagName> - <div className="edit-site-editor__inserter-panel-content"> - <Library - showInserterHelpPanel - shouldFocusBlock={ isMobile } - rootClientId={ insertionPoint.rootClientId } - __experimentalInsertionIndex={ - insertionPoint.insertionIndex - } - __experimentalFilterValue={ insertionPoint.filterValue } - ref={ libraryRef } - /> - </div> - </div> - ); -} diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js deleted file mode 100644 index d18abd0083f07b..00000000000000 --- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js +++ /dev/null @@ -1,121 +0,0 @@ -/** - * WordPress dependencies - */ -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; -import { Button } from '@wordpress/components'; -import { useFocusOnMount, useMergeRefs } from '@wordpress/compose'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { useCallback, useRef, useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { closeSmall } from '@wordpress/icons'; -import { ESCAPE } from '@wordpress/keycodes'; -import { focus } from '@wordpress/dom'; -import { useShortcut } from '@wordpress/keyboard-shortcuts'; -import { store as editorStore } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; - -const { PrivateListView } = unlock( blockEditorPrivateApis ); - -export default function ListViewSidebar() { - const { setIsListViewOpened } = useDispatch( editorStore ); - const { getListViewToggleRef } = unlock( useSelect( editorStore ) ); - - // This hook handles focus when the sidebar first renders. - const focusOnMountRef = useFocusOnMount( 'firstElement' ); - - // When closing the list view, focus should return to the toggle button. - const closeListView = useCallback( () => { - setIsListViewOpened( false ); - getListViewToggleRef().current?.focus(); - }, [ getListViewToggleRef, setIsListViewOpened ] ); - - const closeOnEscape = useCallback( - ( event ) => { - if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) { - event.preventDefault(); - closeListView(); - } - }, - [ closeListView ] - ); - - // Use internal state instead of a ref to make sure that the component - // re-renders when the dropZoneElement updates. - const [ dropZoneElement, setDropZoneElement ] = useState( null ); - - // This ref refers to the sidebar as a whole. - const sidebarRef = useRef(); - // This ref refers to the close button. - const sidebarCloseButtonRef = useRef(); - // This ref refers to the list view application area. - const listViewRef = useRef(); - - /* - * Callback function to handle list view or close button focus. - * - * @return void - */ - function handleSidebarFocus() { - // Either focus the list view or the sidebar close button. Must have a fallback because the list view does not render when there are no blocks. - const listViewApplicationFocus = focus.tabbable.find( - listViewRef.current - )[ 0 ]; - const listViewFocusArea = sidebarRef.current.contains( - listViewApplicationFocus - ) - ? listViewApplicationFocus - : sidebarCloseButtonRef.current; - listViewFocusArea.focus(); - } - - const handleToggleListViewShortcut = useCallback( () => { - // If the sidebar has focus, it is safe to close. - if ( - sidebarRef.current.contains( - sidebarRef.current.ownerDocument.activeElement - ) - ) { - closeListView(); - } else { - // If the list view or close button does not have focus, focus should be moved to it. - handleSidebarFocus(); - } - }, [ closeListView ] ); - - // This only fires when the sidebar is open because of the conditional rendering. - // It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed. - useShortcut( 'core/editor/toggle-list-view', handleToggleListViewShortcut ); - - return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions - <div - className="edit-site-editor__list-view-panel" - onKeyDown={ closeOnEscape } - ref={ sidebarRef } - > - <div className="edit-site-editor__list-view-panel-header"> - <strong>{ __( 'List View' ) }</strong> - <Button - icon={ closeSmall } - label={ __( 'Close' ) } - onClick={ closeListView } - ref={ sidebarCloseButtonRef } - /> - </div> - <div - className="edit-site-editor__list-view-panel-content" - ref={ useMergeRefs( [ - focusOnMountRef, - setDropZoneElement, - listViewRef, - ] ) } - > - <PrivateListView dropZoneElement={ dropZoneElement } /> - </div> - </div> - ); -} diff --git a/packages/edit-site/src/components/secondary-sidebar/style.scss b/packages/edit-site/src/components/secondary-sidebar/style.scss deleted file mode 100644 index 0230c71b2e8009..00000000000000 --- a/packages/edit-site/src/components/secondary-sidebar/style.scss +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Note that this CSS file should be in sync with its counterpart in the other editor: - * packages/edit-post/src/components/secondary-sidebar/style.scss - */ - -.edit-site-editor__inserter-panel, -.edit-site-editor__list-view-panel { - height: 100%; - display: flex; - flex-direction: column; -} - -.edit-site-editor__list-view-panel { - @include break-medium() { - // Same width as the Inserter. - // @see packages/block-editor/src/components/inserter/style.scss - width: 350px; - } -} - -.edit-site-editor__inserter-panel-header { - padding-top: $grid-unit-10; - padding-right: $grid-unit-10; - display: flex; - justify-content: flex-end; -} - -.edit-site-editor__inserter-panel-content, -.edit-site-editor__list-view-panel-content { - // Leave space for the close button - height: calc(100% - #{$button-size} - #{$grid-unit-10}); -} - - -.edit-site-editor__inserter-panel-content { - @include break-medium() { - height: 100%; - } -} - -.edit-site-editor__list-view-panel-header { - align-items: center; - border-bottom: $border-width solid $gray-300; - display: flex; - justify-content: space-between; - height: $grid-unit-60; - padding-left: $grid-unit-20; - padding-right: $grid-unit-05; -} - -.edit-site-editor__list-view-panel-content { - height: 100%; - - // Include custom scrollbars, invisible until hovered. - @include custom-scrollbars-on-hover(transparent, $gray-600); - overflow: auto; - - // Only reserve space for scrollbars when there is content to scroll. - // This allows items in the list view to have equidistant padding left and right - // right up until a scrollbar is present. - scrollbar-gutter: auto; - - // The table cells use an extra pixels of space left and right. We compensate for that here. - padding: $grid-unit-10 ($grid-unit-10 - $border-width - $border-width); -} 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 index d84d85faf4d60a..4bf7a173525c90 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js +++ b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js @@ -19,7 +19,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; * Internal dependencies */ import SidebarNavigationItem from '../sidebar-navigation-item'; -import DEFAULT_VIEWS from './default-views'; +import { DEFAULT_VIEWS } from './default-views'; import { unlock } from '../../lock-unlock'; const { useHistory, useLocation } = unlock( routerPrivateApis ); diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js index 11652286e62d8d..fe9f046f31972f 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -7,10 +7,27 @@ import { trash } from '@wordpress/icons'; /** * Internal dependencies */ -import { LAYOUT_TABLE, OPERATOR_IN } from '../../utils/constants'; +import { + LAYOUT_LIST, + LAYOUT_TABLE, + LAYOUT_GRID, + OPERATOR_IN, +} from '../../utils/constants'; + +export const DEFAULT_CONFIG_PER_VIEW_TYPE = { + [ LAYOUT_TABLE ]: {}, + [ LAYOUT_GRID ]: { + mediaField: 'featured-image', + primaryField: 'title', + }, + [ LAYOUT_LIST ]: { + primaryField: 'title', + mediaField: 'featured-image', + }, +}; const DEFAULT_PAGE_BASE = { - type: LAYOUT_TABLE, + type: LAYOUT_LIST, search: '', filters: [], page: 1, @@ -22,10 +39,12 @@ const DEFAULT_PAGE_BASE = { // All fields are visible by default, so it's // better to keep track of the hidden ones. hiddenFields: [ 'date', 'featured-image' ], - layout: {}, + layout: { + ...DEFAULT_CONFIG_PER_VIEW_TYPE[ LAYOUT_LIST ], + }, }; -const DEFAULT_VIEWS = { +export const DEFAULT_VIEWS = { page: [ { title: __( 'All' ), @@ -55,5 +74,3 @@ const DEFAULT_VIEWS = { }, ], }; - -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 9e4534ab342745..9748600907e331 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/index.js +++ b/packages/edit-site/src/components/sidebar-dataviews/index.js @@ -8,14 +8,14 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; * Internal dependencies */ -import { default as DEFAULT_VIEWS } from './default-views'; +import { DEFAULT_VIEWS } from './default-views'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); import DataViewItem from './dataview-item'; import CustomDataViewsList from './custom-dataviews-list'; const PATH_TO_TYPE = { - '/pages': 'page', + '/page': 'page', }; export default function DataViewsSidebarContent() { 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 48a59935bf17c2..f92ca4c75929cb 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 @@ -5,8 +5,6 @@ import { ComplementaryArea, ComplementaryAreaMoreMenuItem, } from '@wordpress/interface'; -import { useSelect } from '@wordpress/data'; -import { store as preferencesStore } from '@wordpress/preferences'; export default function DefaultSidebar( { className, @@ -19,15 +17,6 @@ export default function DefaultSidebar( { headerClassName, panelClassName, } ) { - const showIconLabels = useSelect( - ( select ) => - !! select( preferencesStore ).get( - 'core/edit-site', - 'showIconLabels' - ), - [] - ); - return ( <> <ComplementaryArea @@ -41,7 +30,6 @@ export default function DefaultSidebar( { header={ header } headerClassName={ headerClassName } panelClassName={ panelClassName } - showIconLabels={ showIconLabels } > { children } </ComplementaryArea> diff --git a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js index 20ae12923d2372..25d10c51645fc2 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js @@ -44,7 +44,7 @@ export default function GlobalStylesSidebar() { 'visual' === select( editSiteStore ).getEditorMode(); const _isEditCanvasMode = 'edit' === getCanvasMode(); const _showListViewByDefault = select( preferencesStore ).get( - 'core/edit-site', + 'core', 'showListViewByDefault' ); const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = 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 45a3f6d48d0b98..23eafa7b473054 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 @@ -2,8 +2,6 @@ * WordPress dependencies */ import { ComplementaryArea } from '@wordpress/interface'; -import { useSelect } from '@wordpress/data'; -import { store as preferencesStore } from '@wordpress/preferences'; /** * Renders a sidebar when activated. The contents within the `PluginSidebar` will appear as content within the sidebar. @@ -71,21 +69,11 @@ import { store as preferencesStore } from '@wordpress/preferences'; * ``` */ export default function PluginSidebarEditSite( { className, ...props } ) { - const showIconLabels = useSelect( - ( select ) => - !! select( preferencesStore ).get( - 'core/edit-site', - 'showIconLabels' - ), - [] - ); - return ( <ComplementaryArea panelClassName={ className } className="edit-site-sidebar-edit-mode" scope="core/edit-site" - showIconLabels={ showIconLabels } { ...props } /> ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js index 110bc920fb0a9f..7df1aaa3ba9084 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js @@ -31,7 +31,7 @@ import SidebarNavigationScreenDetailsFooter from '../sidebar-navigation-screen-d const { useHistory } = unlock( routerPrivateApis ); -export default function SidebarNavigationScreenPage() { +export default function SidebarNavigationScreenPage( { backPath } ) { const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const history = useHistory(); const { @@ -88,6 +88,7 @@ export default function SidebarNavigationScreenPage() { return record ? ( <SidebarNavigationScreen + backPath={ backPath } title={ decodeEntities( record?.title?.rendered || __( '(no title)' ) ) } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages-dataviews/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pages-dataviews/index.js new file mode 100644 index 00000000000000..171d59c108e9b8 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pages-dataviews/index.js @@ -0,0 +1,77 @@ +/** + * WordPress dependencies + */ +import { + __experimentalTruncate as Truncate, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { layout } from '@wordpress/icons'; +import { useMemo } from '@wordpress/element'; +import { useEntityRecords } from '@wordpress/core-data'; +import { decodeEntities } from '@wordpress/html-entities'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useLink } from '../routes/link'; +import { TEMPLATE_POST_TYPE } from '../../utils/constants'; +import SidebarNavigationItem from '../sidebar-navigation-item'; +import SidebarNavigationScreen from '../sidebar-navigation-screen'; +import DataViewsSidebarContent from '../sidebar-dataviews'; + +const PageItem = ( { postType = 'page', postId, ...props } ) => { + const linkInfo = useLink( + { + postType, + postId, + }, + { + backPath: '/page', + } + ); + return <SidebarNavigationItem { ...linkInfo } { ...props } />; +}; + +export default function SidebarNavigationScreenPagesDataViews() { + const { records: templateRecords } = useEntityRecords( + 'postType', + TEMPLATE_POST_TYPE, + { + per_page: -1, + } + ); + const templates = useMemo( + () => + templateRecords?.filter( ( { slug } ) => + [ '404', 'search' ].includes( slug ) + ), + [ templateRecords ] + ); + + return ( + <SidebarNavigationScreen + title={ __( 'Pages' ) } + content={ <DataViewsSidebarContent /> } + footer={ + <VStack spacing={ 0 }> + { templates?.map( ( item ) => ( + <PageItem + postType={ TEMPLATE_POST_TYPE } + postId={ item.id } + key={ item.id } + icon={ layout } + withChevron + > + <Truncate numberOfLines={ 1 }> + { decodeEntities( + item.title?.rendered || __( '(no title)' ) + ) } + </Truncate> + </PageItem> + ) ) } + </VStack> + } + /> + ); +} diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index 3fa1280d59f427..73c6aea7e328c5 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -7,7 +7,6 @@ import classNames from 'classnames'; * WordPress dependencies */ import { memo, useRef } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; import { __experimentalNavigatorProvider as NavigatorProvider, __experimentalNavigatorScreen as NavigatorScreen, @@ -32,9 +31,8 @@ import SidebarNavigationScreenTemplatesBrowse from '../sidebar-navigation-screen import SaveHub from '../save-hub'; import { unlock } from '../../lock-unlock'; import SidebarNavigationScreenPages from '../sidebar-navigation-screen-pages'; +import SidebarNavigationScreenPagesDataViews from '../sidebar-navigation-screen-pages-dataviews'; import SidebarNavigationScreenPage from '../sidebar-navigation-screen-page'; -import SidebarNavigationScreen from '../sidebar-navigation-screen'; -import DataViewsSidebarContent from '../sidebar-dataviews'; const { useLocation } = unlock( routerPrivateApis ); @@ -68,20 +66,15 @@ function SidebarScreens() { <SidebarNavigationScreenGlobalStyles /> </SidebarScreenWrapper> <SidebarScreenWrapper path="/page"> - <SidebarNavigationScreenPages /> + { window?.__experimentalAdminViews ? ( + <SidebarNavigationScreenPagesDataViews /> + ) : ( + <SidebarNavigationScreenPages /> + ) } </SidebarScreenWrapper> <SidebarScreenWrapper path="/page/:postId"> <SidebarNavigationScreenPage /> </SidebarScreenWrapper> - { window?.__experimentalAdminViews && ( - <SidebarScreenWrapper path="/pages"> - <SidebarNavigationScreen - title={ __( 'Pages' ) } - backPath="/page" - content={ <DataViewsSidebarContent /> } - /> - </SidebarScreenWrapper> - ) } <SidebarScreenWrapper path="/:postType(wp_template)"> <SidebarNavigationScreenTemplates /> </SidebarScreenWrapper> diff --git a/packages/edit-site/src/components/site-hub/style.scss b/packages/edit-site/src/components/site-hub/style.scss index 49e5304d1688fc..0fda40c2e8a9a0 100644 --- a/packages/edit-site/src/components/site-hub/style.scss +++ b/packages/edit-site/src/components/site-hub/style.scss @@ -73,7 +73,10 @@ .edit-site-site-hub_toggle-command-center { color: $gray-200; - &:hover { - color: $gray-100; + &:hover, + &:active { + svg { + fill: $gray-100; + } } } diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index d3e0cb3531ae99..7ecfdc3506cf8a 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -8,7 +8,6 @@ import classnames from 'classnames'; */ import { Disabled, - TabPanel, privateApis as componentsPrivateApis, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; @@ -48,6 +47,7 @@ const { CompositeV2: Composite, CompositeItemV2: CompositeItem, useCompositeStoreV2: useCompositeStore, + Tabs, } = unlock( componentsPrivateApis ); // The content area of the Style Book is rendered within an iframe so that global styles @@ -253,22 +253,37 @@ function StyleBook( { > { resizeObserver } { showTabs ? ( - <TabPanel - className="edit-site-style-book__tab-panel" - tabs={ tabs } - > - { ( tab ) => ( - <StyleBookBody - category={ tab.name } - examples={ examples } - isSelected={ isSelected } - onSelect={ onSelect } - settings={ settings } - sizes={ sizes } - title={ tab.title } - /> - ) } - </TabPanel> + <div className="edit-site-style-book__tabs"> + <Tabs> + <Tabs.TabList> + { tabs.map( ( tab ) => ( + <Tabs.Tab + tabId={ tab.name } + key={ tab.name } + > + { tab.title } + </Tabs.Tab> + ) ) } + </Tabs.TabList> + { tabs.map( ( tab ) => ( + <Tabs.TabPanel + key={ tab.name } + tabId={ tab.name } + focusable={ false } + > + <StyleBookBody + category={ tab.name } + examples={ examples } + isSelected={ isSelected } + onSelect={ onSelect } + settings={ settings } + sizes={ sizes } + title={ tab.title } + /> + </Tabs.TabPanel> + ) ) } + </Tabs> + </div> ) : ( <StyleBookBody examples={ examples } diff --git a/packages/edit-site/src/components/style-book/style.scss b/packages/edit-site/src/components/style-book/style.scss index 0ddefb055a8d8d..3b2c6ab0867dbe 100644 --- a/packages/edit-site/src/components/style-book/style.scss +++ b/packages/edit-site/src/components/style-book/style.scss @@ -17,13 +17,13 @@ } } -.edit-site-style-book__tab-panel { - .components-tab-panel__tabs { +.edit-site-style-book__tabs { + [role="tablist"] { background: $white; color: $gray-900; } - .components-tab-panel__tab-content { + [role="tabpanel"] { bottom: 0; left: 0; overflow: auto; diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js index ffd9907160d26c..0265329f40b095 100644 --- a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js +++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js @@ -217,6 +217,7 @@ function useEditUICommands() { isListViewOpen, isDistractionFree, } = useSelect( ( select ) => { + const { get } = select( preferencesStore ); const { getEditorMode } = select( editSiteStore ); const { isListViewOpened } = select( editorStore ); return { @@ -225,15 +226,9 @@ function useEditUICommands() { activeSidebar: select( interfaceStore ).getActiveComplementaryArea( editSiteStore.name ), - showBlockBreadcrumbs: select( preferencesStore ).get( - 'core/edit-site', - 'showBlockBreadcrumbs' - ), + showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), isListViewOpen: isListViewOpened(), - isDistractionFree: select( preferencesStore ).get( - editSiteStore.name, - 'distractionFree' - ), + isDistractionFree: get( 'core', 'distractionFree' ), }; }, [] ); const { openModal } = useDispatch( interfaceStore ); @@ -278,7 +273,7 @@ function useEditUICommands() { name: 'core/toggle-spotlight-mode', label: __( 'Toggle spotlight mode' ), callback: ( { close } ) => { - toggle( 'core/edit-site', 'focusMode' ); + toggle( 'core', 'focusMode' ); close(); }, } ); @@ -296,7 +291,7 @@ function useEditUICommands() { name: 'core/toggle-top-toolbar', label: __( 'Toggle top toolbar' ), callback: ( { close } ) => { - toggle( 'core/edit-site', 'fixedToolbar' ); + toggle( 'core', 'fixedToolbar' ); if ( isDistractionFree ) { toggleDistractionFree(); } @@ -339,7 +334,7 @@ function useEditUICommands() { ? __( 'Hide block breadcrumbs' ) : __( 'Show block breadcrumbs' ), callback: ( { close } ) => { - toggle( 'core/edit-site', 'showBlockBreadcrumbs' ); + toggle( 'core', 'showBlockBreadcrumbs' ); close(); createInfoNotice( showBlockBreadcrumbs diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 9dcdb2ce99b5bc..29b7df32e6d693 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -52,18 +52,23 @@ export function initializeEditor( id, settings ) { // We dispatch actions and update the store synchronously before rendering // so that we won't trigger unnecessary re-renders with useEffect. dispatch( preferencesStore ).setDefaults( 'core/edit-site', { - allowRightClickOverrides: true, editorMode: 'visual', - fixedToolbar: false, - focusMode: false, - distractionFree: false, - keepCaretInsideBlock: false, welcomeGuide: true, welcomeGuideStyles: true, welcomeGuidePage: true, welcomeGuideTemplate: true, - showListViewByDefault: false, + } ); + + dispatch( preferencesStore ).setDefaults( 'core', { + allowRightClickOverrides: true, + distractionFree: false, + fixedToolbar: false, + focusMode: false, + inactivePanels: [], + keepCaretInsideBlock: false, + openPanels: [ 'post-status' ], showBlockBreadcrumbs: true, + showListViewByDefault: false, } ); dispatch( interfaceStore ).setDefaultComplementaryArea( diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 820c6f5239cce4..5a8adad8e198b8 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -524,7 +524,7 @@ export const openGeneralSidebar = ( { dispatch, registry } ) => { const isDistractionFree = registry .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ); + .get( 'core', 'distractionFree' ); if ( isDistractionFree ) { dispatch.toggleDistractionFree(); } @@ -561,7 +561,7 @@ export const switchEditorMode = } else if ( mode === 'text' ) { const isDistractionFree = registry .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ); + .get( 'core', 'distractionFree' ); if ( isDistractionFree ) { dispatch.toggleDistractionFree(); } @@ -602,12 +602,12 @@ export const toggleDistractionFree = ( { dispatch, registry } ) => { const isDistractionFree = registry .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ); + .get( 'core', 'distractionFree' ); if ( ! isDistractionFree ) { registry.batch( () => { registry .dispatch( preferencesStore ) - .set( 'core/edit-site', 'fixedToolbar', true ); + .set( 'core', 'fixedToolbar', true ); registry.dispatch( editorStore ).setIsInserterOpened( false ); registry.dispatch( editorStore ).setIsListViewOpened( false ); dispatch.closeGeneralSidebar(); @@ -616,11 +616,7 @@ export const toggleDistractionFree = registry.batch( () => { registry .dispatch( preferencesStore ) - .set( - 'core/edit-site', - 'distractionFree', - ! isDistractionFree - ); + .set( 'core', 'distractionFree', ! isDistractionFree ); registry .dispatch( noticesStore ) .createInfoNotice( diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index eb945fa748555e..7354f7b9b8843a 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -24,10 +24,10 @@ export const setCanvasMode = mode === 'edit' && registry .select( preferencesStore ) - .get( 'core/edit-site', 'showListViewByDefault' ) && + .get( 'core', 'showListViewByDefault' ) && ! registry .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ) + .get( 'core', 'distractionFree' ) ) { registry.dispatch( editorStore ).setIsListViewOpened( true ); } else { diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index a0ea9b2fe0885f..b3612b9a801ccd 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -81,14 +81,14 @@ describe( 'actions', () => { const registry = createRegistryWithStores(); registry .dispatch( preferencesStore ) - .set( 'core/edit-site', 'distractionFree', true ); + .set( 'core', 'distractionFree', true ); registry .dispatch( editSiteStore ) .openGeneralSidebar( 'edit-site/global-styles' ); expect( registry .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ) + .get( 'core', 'distractionFree' ) ).toBe( false ); } ); } ); @@ -98,18 +98,18 @@ describe( 'actions', () => { const registry = createRegistryWithStores(); registry .dispatch( preferencesStore ) - .set( 'core/edit-site', 'distractionFree', true ); + .set( 'core', 'distractionFree', true ); registry.dispatch( editSiteStore ).switchEditorMode( 'visual' ); expect( registry .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ) + .get( 'core', 'distractionFree' ) ).toBe( true ); registry.dispatch( editSiteStore ).switchEditorMode( 'text' ); expect( registry .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ) + .get( 'core', 'distractionFree' ) ).toBe( false ); } ); } ); @@ -120,7 +120,7 @@ describe( 'actions', () => { // Enable everything that shouldn't be enabled in distraction free mode. registry .dispatch( preferencesStore ) - .set( 'core/edit-site', 'fixedToolbar', true ); + .set( 'core', 'fixedToolbar', true ); registry.dispatch( editorStore ).setIsListViewOpened( true ); registry .dispatch( editSiteStore ) @@ -130,7 +130,7 @@ describe( 'actions', () => { expect( registry .select( preferencesStore ) - .get( 'core/edit-site', 'fixedToolbar' ) + .get( 'core', 'fixedToolbar' ) ).toBe( true ); expect( registry.select( editorStore ).isListViewOpened() ).toBe( false @@ -146,7 +146,7 @@ describe( 'actions', () => { expect( registry .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ) + .get( 'core', 'distractionFree' ) ).toBe( true ); } ); } ); diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 53c4f672ed8c23..c7d0609b4e771c 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -21,7 +21,6 @@ @import "./components/sidebar-edit-mode/template-panel/style.scss"; @import "./components/editor/style.scss"; @import "./components/create-template-part-modal/style.scss"; -@import "./components/secondary-sidebar/style.scss"; @import "./components/welcome-guide/style.scss"; @import "./components/start-template-options/style.scss"; @import "./components/keyboard-shortcut-help-modal/style.scss"; diff --git a/packages/edit-site/src/utils/get-is-list-page.js b/packages/edit-site/src/utils/get-is-list-page.js index 2ee661253cf063..9530cd85bf04b4 100644 --- a/packages/edit-site/src/utils/get-is-list-page.js +++ b/packages/edit-site/src/utils/get-is-list-page.js @@ -14,9 +14,8 @@ export default function getIsListPage( isMobileViewport ) { return ( - [ '/wp_template/all', '/wp_template_part/all', '/pages' ].includes( - path - ) || + [ '/wp_template/all', '/wp_template_part/all' ].includes( path ) || + ( path === '/page' && window?.__experimentalAdminViews ) || ( path === '/patterns' && // Don't treat "/patterns" without categoryType and categoryId as a // list page in mobile because the sidebar covers the whole page. diff --git a/packages/edit-widgets/CHANGELOG.md b/packages/edit-widgets/CHANGELOG.md index 51ce53bda4a07e..34fb6a54d8c25d 100644 --- a/packages/edit-widgets/CHANGELOG.md +++ b/packages/edit-widgets/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 5.26.0 (2023-12-27) - ## 5.25.0 (2023-12-13) ## 5.24.0 (2023-11-29) diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index 85b553fb090a8b..974a0ba02905fe 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.26.0-prerelease", + "version": "5.25.0", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-widgets/src/store/private-selectors.js b/packages/edit-widgets/src/store/private-selectors.js index 29911091622fbb..fca6aa5ddb759c 100644 --- a/packages/edit-widgets/src/store/private-selectors.js +++ b/packages/edit-widgets/src/store/private-selectors.js @@ -1,16 +1,3 @@ -/** - * External dependencies - */ -import createSelector from 'rememo'; - -/** - * WordPress dependencies - */ -import { createRef } from '@wordpress/element'; - -export const getListViewToggleRef = createSelector( - () => { - return createRef(); - }, - () => [] -); +export function getListViewToggleRef( state ) { + return state.listViewToggleRef; +} diff --git a/packages/edit-widgets/src/store/reducer.js b/packages/edit-widgets/src/store/reducer.js index ff942b955edcdc..64bd6b4e0400e7 100644 --- a/packages/edit-widgets/src/store/reducer.js +++ b/packages/edit-widgets/src/store/reducer.js @@ -68,6 +68,17 @@ export function listViewPanel( state = false, action ) { return state; } +/** + * This reducer does nothing aside initializing a ref to the list view toggle. + * We will have a unique ref per "editor" instance. + * + * @param {Object} state + * @return {Object} Reference to the list view toggle button. + */ +export function listViewToggleRef( state = { current: null } ) { + return state; +} + export default combineReducers( { blockInserterPanel, listViewPanel, diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 4c1b821e872202..dc8fffd379798a 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 13.26.0 (2023-12-27) - ### New Features - Add the editor panels visibility state to the editor store in addition to the following actions and selectors: `toggleEditorPanelEnabled`, `toggleEditorPanelOpened`, `removeEditorPanel`, `isEditorPanelRemoved`, `isEditorPanelOpened` and `isEditorPanelEnabled`. diff --git a/packages/editor/package.json b/packages/editor/package.json index 72d79660f7c76d..63656899e587c0 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.26.0-prerelease", + "version": "13.25.0", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/src/components/document-bar/index.js b/packages/editor/src/components/document-bar/index.js index da43533bfa5bc2..fdf9cbe55dbca7 100644 --- a/packages/editor/src/components/document-bar/index.js +++ b/packages/editor/src/components/document-bar/index.js @@ -48,40 +48,51 @@ const icons = { }; export default function DocumentBar() { - const { isEditingTemplate, templateId, postType, postId } = useSelect( - ( select ) => { - const { - getRenderingMode, - getCurrentTemplateId, - getCurrentPostId, - getCurrentPostType, - } = select( editorStore ); - const _templateId = getCurrentTemplateId(); - return { - isEditingTemplate: - !! _templateId && getRenderingMode() === 'template-only', - templateId: _templateId, - postType: getCurrentPostType(), - postId: getCurrentPostId(), - }; - }, - [] - ); - const { getEditorSettings } = useSelect( editorStore ); + const { + isEditingTemplate, + templateId, + postType, + postId, + goBack, + getEditorSettings, + } = useSelect( ( select ) => { + const { + getRenderingMode, + getCurrentTemplateId, + getCurrentPostId, + getCurrentPostType, + getEditorSettings: getSettings, + } = select( editorStore ); + const _templateId = getCurrentTemplateId(); + const back = getSettings().goBack; + return { + isEditingTemplate: + !! _templateId && getRenderingMode() === 'template-only', + templateId: _templateId, + postType: getCurrentPostType(), + postId: getCurrentPostId(), + goBack: typeof back === 'function' ? back : undefined, + getEditorSettings: getSettings, + }; + }, [] ); + const { setRenderingMode } = useDispatch( editorStore ); + const handleOnBack = () => { + if ( isEditingTemplate ) { + setRenderingMode( getEditorSettings().defaultRenderingMode ); + return; + } + if ( goBack ) { + goBack(); + } + }; + return ( <BaseDocumentActions postType={ isEditingTemplate ? 'wp_template' : postType } postId={ isEditingTemplate ? templateId : postId } - onBack={ - isEditingTemplate - ? () => - setRenderingMode( - getEditorSettings().defaultRenderingMode - ) - : undefined - } + onBack={ isEditingTemplate || goBack ? handleOnBack : undefined } /> ); } diff --git a/packages/editor/src/components/document-outline/index.js b/packages/editor/src/components/document-outline/index.js index cd7c828cb7d3b3..bbddf220115406 100644 --- a/packages/editor/src/components/document-outline/index.js +++ b/packages/editor/src/components/document-outline/index.js @@ -7,6 +7,7 @@ import { withSelect, useDispatch } from '@wordpress/data'; import { create, getTextContent } from '@wordpress/rich-text'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; +import { Path, SVG, Line, Rect } from '@wordpress/components'; /** * Internal dependencies @@ -34,6 +35,43 @@ const multipleH1Headings = [ { __( '(Multiple H1 headings are not recommended)' ) } </em>, ]; +function EmptyOutlineIllustration() { + return ( + <SVG + width="138" + height="148" + viewBox="0 0 138 148" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <Rect width="138" height="148" rx="4" fill="#F0F6FC" /> + <Line x1="44" y1="28" x2="24" y2="28" stroke="#DDDDDD" /> + <Rect x="48" y="16" width="27" height="23" rx="4" fill="#DDDDDD" /> + <Path + d="M54.7585 32V23.2727H56.6037V26.8736H60.3494V23.2727H62.1903V32H60.3494V28.3949H56.6037V32H54.7585ZM67.4574 23.2727V32H65.6122V25.0241H65.5611L63.5625 26.277V24.6406L65.723 23.2727H67.4574Z" + fill="black" + /> + <Line x1="55" y1="59" x2="24" y2="59" stroke="#DDDDDD" /> + <Rect x="59" y="47" width="29" height="23" rx="4" fill="#DDDDDD" /> + <Path + d="M65.7585 63V54.2727H67.6037V57.8736H71.3494V54.2727H73.1903V63H71.3494V59.3949H67.6037V63H65.7585ZM74.6605 63V61.6705L77.767 58.794C78.0313 58.5384 78.2528 58.3082 78.4318 58.1037C78.6136 57.8991 78.7514 57.6989 78.8452 57.5028C78.9389 57.304 78.9858 57.0895 78.9858 56.8594C78.9858 56.6037 78.9276 56.3835 78.8111 56.1989C78.6946 56.0114 78.5355 55.8679 78.3338 55.7685C78.1321 55.6662 77.9034 55.6151 77.6477 55.6151C77.3807 55.6151 77.1477 55.669 76.9489 55.777C76.75 55.8849 76.5966 56.0398 76.4886 56.2415C76.3807 56.4432 76.3267 56.6832 76.3267 56.9616H74.5753C74.5753 56.3906 74.7045 55.8949 74.9631 55.4744C75.2216 55.054 75.5838 54.7287 76.0497 54.4986C76.5156 54.2685 77.0526 54.1534 77.6605 54.1534C78.2855 54.1534 78.8295 54.2642 79.2926 54.4858C79.7585 54.7045 80.1207 55.0085 80.3793 55.3977C80.6378 55.7869 80.767 56.233 80.767 56.7358C80.767 57.0653 80.7017 57.3906 80.571 57.7116C80.4432 58.0327 80.2145 58.3892 79.8849 58.7812C79.5554 59.1705 79.0909 59.6378 78.4915 60.1832L77.2173 61.4318V61.4915H80.8821V63H74.6605Z" + fill="black" + /> + <Line x1="80" y1="90" x2="24" y2="90" stroke="#DDDDDD" /> + <Rect x="84" y="78" width="30" height="23" rx="4" fill="#F0B849" /> + <Path + d="M90.7585 94V85.2727H92.6037V88.8736H96.3494V85.2727H98.1903V94H96.3494V90.3949H92.6037V94H90.7585ZM99.5284 92.4659V91.0128L103.172 85.2727H104.425V87.2841H103.683L101.386 90.919V90.9872H106.564V92.4659H99.5284ZM103.717 94V92.0227L103.751 91.3793V85.2727H105.482V94H103.717Z" + fill="black" + /> + <Line x1="66" y1="121" x2="24" y2="121" stroke="#DDDDDD" /> + <Rect x="70" y="109" width="29" height="23" rx="4" fill="#DDDDDD" /> + <Path + d="M76.7585 125V116.273H78.6037V119.874H82.3494V116.273H84.1903V125H82.3494V121.395H78.6037V125H76.7585ZM88.8864 125.119C88.25 125.119 87.6832 125.01 87.1861 124.791C86.6918 124.57 86.3011 124.266 86.0142 123.879C85.7301 123.49 85.5838 123.041 85.5753 122.533H87.4332C87.4446 122.746 87.5142 122.933 87.642 123.095C87.7727 123.254 87.946 123.378 88.1619 123.466C88.3778 123.554 88.6207 123.598 88.8906 123.598C89.1719 123.598 89.4205 123.548 89.6364 123.449C89.8523 123.349 90.0213 123.212 90.1435 123.036C90.2656 122.859 90.3267 122.656 90.3267 122.426C90.3267 122.193 90.2614 121.987 90.1307 121.808C90.0028 121.626 89.8182 121.484 89.5767 121.382C89.3381 121.28 89.054 121.229 88.7244 121.229H87.9105V119.874H88.7244C89.0028 119.874 89.2486 119.825 89.4616 119.729C89.6776 119.632 89.8452 119.499 89.9645 119.328C90.0838 119.155 90.1435 118.953 90.1435 118.723C90.1435 118.504 90.0909 118.312 89.9858 118.148C89.8835 117.98 89.7386 117.849 89.5511 117.756C89.3665 117.662 89.1506 117.615 88.9034 117.615C88.6534 117.615 88.4247 117.661 88.2173 117.751C88.0099 117.839 87.8438 117.966 87.7188 118.131C87.5938 118.295 87.527 118.489 87.5185 118.71H85.75C85.7585 118.207 85.902 117.764 86.1804 117.381C86.4588 116.997 86.8338 116.697 87.3054 116.482C87.7798 116.263 88.3153 116.153 88.9119 116.153C89.5142 116.153 90.0412 116.263 90.4929 116.482C90.9446 116.7 91.2955 116.996 91.5455 117.368C91.7983 117.737 91.9233 118.152 91.9205 118.612C91.9233 119.101 91.7713 119.509 91.4645 119.835C91.1605 120.162 90.7642 120.369 90.2756 120.457V120.526C90.9176 120.608 91.4063 120.831 91.7415 121.195C92.0795 121.555 92.2472 122.007 92.2443 122.55C92.2472 123.047 92.1037 123.489 91.8139 123.875C91.527 124.261 91.1307 124.565 90.625 124.787C90.1193 125.009 89.5398 125.119 88.8864 125.119Z" + fill="black" + /> + </SVG> + ); +} /** * Returns an array of heading blocks enhanced with the following properties: @@ -70,7 +108,16 @@ export const DocumentOutline = ( { const headings = computeOutlineHeadings( blocks ); const { selectBlock } = useDispatch( blockEditorStore ); if ( headings.length < 1 ) { - return null; + return ( + <div className="editor-document-outline has-no-headings"> + <EmptyOutlineIllustration /> + <p> + { __( + 'Navigate the structure of your document and address issues like empty or incorrect heading levels.' + ) } + </p> + </div> + ); } let prevHeadingLevel = 1; diff --git a/packages/editor/src/components/document-outline/style.scss b/packages/editor/src/components/document-outline/style.scss index 852b40b58eb8f8..efd2606b82d394 100644 --- a/packages/editor/src/components/document-outline/style.scss +++ b/packages/editor/src/components/document-outline/style.scss @@ -81,3 +81,15 @@ .document-outline__item-content { padding: 1px 0; } + +.editor-document-outline.has-no-headings { + & > svg { + margin-top: $grid-unit-30 + $grid-unit-05; + } + & > p { + padding-left: $grid-unit-40; + padding-right: $grid-unit-40; + } + text-align: center; + color: $gray-700; +} diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/editor/src/components/document-tools/index.js similarity index 66% rename from packages/edit-post/src/components/header/header-toolbar/index.js rename to packages/editor/src/components/document-tools/index.js index e8786900e4f257..cf26fc600a0385 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/editor/src/components/document-tools/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -10,21 +15,19 @@ import { store as blockEditorStore, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { - EditorHistoryRedo, - EditorHistoryUndo, - store as editorStore, -} from '@wordpress/editor'; 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 */ -import { store as editPostStore } from '../../../store'; -import { unlock } from '../../../lock-unlock'; +import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; +import EditorHistoryRedo from '../editor-history/redo'; +import EditorHistoryUndo from '../editor-history/undo'; const { useCanBlockToolbarBeFocused } = unlock( blockEditorPrivateApis ); @@ -32,42 +35,40 @@ const preventDefault = ( event ) => { event.preventDefault(); }; -function HeaderToolbar( { hasFixedToolbar } ) { +function DocumentTools( { + className, + disableBlockTools = false, + children, + // This is a temporary prop until the list view is fully unified between post and site editors. + listViewLabel = __( 'Document Overview' ), +} ) { const inserterButton = useRef(); const { setIsInserterOpened, setIsListViewOpened } = useDispatch( editorStore ); const { - isInserterEnabled, isInserterOpened, - isTextModeEnabled, - showIconLabels, isListViewOpen, listViewShortcut, listViewToggleRef, + hasFixedToolbar, + showIconLabels, } = useSelect( ( select ) => { - const { hasInserterItems, getBlockRootClientId, getBlockSelectionEnd } = - select( blockEditorStore ); - const { getEditorSettings, isListViewOpened, getListViewToggleRef } = - unlock( select( editorStore ) ); - const { getEditorMode, isFeatureActive } = select( editPostStore ); + const { getSettings } = select( blockEditorStore ); + const { get } = select( preferencesStore ); + const { isListViewOpened, getListViewToggleRef } = unlock( + select( editorStore ) + ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); return { - // This setting (richEditingEnabled) should not live in the block editor's setting. - isInserterEnabled: - getEditorMode() === 'visual' && - getEditorSettings().richEditingEnabled && - hasInserterItems( - getBlockRootClientId( getBlockSelectionEnd() ) - ), isInserterOpened: select( editorStore ).isInserterOpened(), - isTextModeEnabled: getEditorMode() === 'text', - showIconLabels: isFeatureActive( 'showIconLabels' ), isListViewOpen: isListViewOpened(), listViewShortcut: getShortcutRepresentation( 'core/editor/toggle-list-view' ), listViewToggleRef: getListViewToggleRef(), + hasFixedToolbar: getSettings().hasFixedToolbar, + showIconLabels: get( 'core', 'showIconLabels' ), }; }, [] ); @@ -82,26 +83,7 @@ function HeaderToolbar( { hasFixedToolbar } ) { () => setIsListViewOpened( ! isListViewOpen ), [ setIsListViewOpened, isListViewOpen ] ); - const overflowItems = ( - <> - <ToolbarItem - as={ Button } - className="edit-post-header-toolbar__document-overview-toggle" - icon={ listView } - disabled={ isTextModeEnabled } - isPressed={ isListViewOpen } - /* translators: button label text should, if possible, be under 16 characters. */ - label={ __( 'Document Overview' ) } - onClick={ toggleListView } - shortcut={ listViewShortcut } - showTooltip={ ! showIconLabels } - variant={ showIconLabels ? 'tertiary' : undefined } - aria-expanded={ isListViewOpen } - ref={ listViewToggleRef } - size="compact" - /> - </> - ); + const toggleInserter = useCallback( () => { if ( isInserterOpened ) { // Focusing the inserter button should close the inserter popover. @@ -123,21 +105,21 @@ function HeaderToolbar( { hasFixedToolbar } ) { return ( <NavigableToolbar - className="edit-post-header-toolbar" + className={ classnames( 'editor-document-tools', className ) } aria-label={ toolbarAriaLabel } shouldUseKeyboardFocusShortcut={ ! blockToolbarCanBeFocused } variant="unstyled" > - <div className="edit-post-header-toolbar__left"> + <div className="editor-document-tools__left"> <ToolbarItem ref={ inserterButton } as={ Button } - className="edit-post-header-toolbar__inserter-toggle" + className="editor-document-tools__inserter-toggle" variant="primary" isPressed={ isInserterOpened } onMouseDown={ preventDefault } onClick={ toggleInserter } - disabled={ ! isInserterEnabled } + disabled={ disableBlockTools } icon={ plus } label={ showIconLabels ? shortLabel : longLabel } showTooltip={ ! showIconLabels } @@ -152,7 +134,7 @@ function HeaderToolbar( { hasFixedToolbar } ) { variant={ showIconLabels ? 'tertiary' : undefined } - disabled={ isTextModeEnabled } + disabled={ disableBlockTools } size="compact" /> ) } @@ -168,12 +150,28 @@ function HeaderToolbar( { hasFixedToolbar } ) { variant={ showIconLabels ? 'tertiary' : undefined } size="compact" /> - { overflowItems } + <ToolbarItem + as={ Button } + className="editor-document-tools__document-overview-toggle" + icon={ listView } + disabled={ disableBlockTools } + isPressed={ isListViewOpen } + /* translators: button label text should, if possible, be under 16 characters. */ + label={ listViewLabel } + onClick={ toggleListView } + shortcut={ listViewShortcut } + showTooltip={ ! showIconLabels } + variant={ showIconLabels ? 'tertiary' : undefined } + aria-expanded={ isListViewOpen } + ref={ listViewToggleRef } + size="compact" + /> </> ) } + { children } </div> </NavigableToolbar> ); } -export default HeaderToolbar; +export default DocumentTools; diff --git a/packages/edit-post/src/components/header/header-toolbar/style.scss b/packages/editor/src/components/document-tools/style.scss similarity index 63% rename from packages/edit-post/src/components/header/header-toolbar/style.scss rename to packages/editor/src/components/document-tools/style.scss index 717d5cd760db58..2aa39b1cbed858 100644 --- a/packages/edit-post/src/components/header/header-toolbar/style.scss +++ b/packages/editor/src/components/document-tools/style.scss @@ -1,9 +1,9 @@ -.edit-post-header-toolbar { +.editor-document-tools { display: inline-flex; align-items: center; // Hide all action buttons except the inserter on mobile. - .edit-post-header-toolbar__left > .components-button { + .editor-document-tools__left > .components-button { display: none; @include break-small() { @@ -11,7 +11,7 @@ } } - .edit-post-header-toolbar__left > .edit-post-header-toolbar__inserter-toggle { + .editor-document-tools__left > .editor-document-tools__inserter-toggle { display: inline-flex; svg { @@ -37,8 +37,8 @@ // The Toolbar component adds different styles to buttons, so we reset them // here to the original button styles - .edit-post-header-toolbar__left > .components-button.has-icon, - .edit-post-header-toolbar__left > .components-dropdown > .components-button.has-icon { + .editor-document-tools__left > .components-button.has-icon, + .editor-document-tools__left > .components-dropdown > .components-button.has-icon { // @todo: override toolbar group inherited paddings from components/block-tools/style.scss. // This is best fixed by making the mover control area a proper single toolbar group. // It needs specificity due to style inherited from .components-accessible-toolbar .components-button.has-icon.has-icon. @@ -61,25 +61,7 @@ } } -// Reduced UI. -.edit-post-header.has-reduced-ui { - @include break-small () { - // Apply transition to every button but the first one. - .edit-post-header-toolbar__left > * + .components-button, - .edit-post-header-toolbar__left > * + .components-dropdown > [aria-expanded="false"] { - transition: opacity 0.1s linear; - @include reduce-motion("transition"); - } - - // Zero out opacity unless hovered. - &:not(:hover) .edit-post-header-toolbar__left > * + .components-button, - &:not(:hover) .edit-post-header-toolbar__left > * + .components-dropdown > [aria-expanded="false"] { - opacity: 0; - } - } -} - -.edit-post-header-toolbar__left { +.editor-document-tools__left { display: inline-flex; align-items: center; padding-left: $grid-unit-20; @@ -98,7 +80,7 @@ } } -.edit-post-header-toolbar .edit-post-header-toolbar__left > .edit-post-header-toolbar__inserter-toggle.has-icon { +.editor-document-tools .editor-document-tools__left > .editor-document-tools__inserter-toggle.has-icon { min-width: $button-size-compact; width: $button-size-compact; height: $button-size-compact; @@ -111,6 +93,6 @@ } } -.show-icon-labels .edit-post-header-toolbar__left > * + * { +.show-icon-labels .editor-document-tools__left > * + * { margin-left: $grid-unit-10; } diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index cd87db0d4bf5e3..e5e663f821a649 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -37,6 +37,8 @@ const { ExperimentalBlockCanvas: BlockCanvas, } = unlock( blockEditorPrivateApis ); +const noop = () => {}; + /** * Given an array of nested blocks, find the first Post Content * block inside it, recursing through any nesting levels, @@ -89,6 +91,7 @@ function EditorCanvas( { wrapperBlockName, wrapperUniqueId, deviceType, + hasHistory, } = useSelect( ( select ) => { const { getCurrentPostId, @@ -125,7 +128,7 @@ function EditorCanvas( { return { renderingMode: _renderingMode, - postContentAttributes: getEditorSettings().postContentAttributes, + postContentAttributes: editorSettings.postContentAttributes, // Post template fetch returns a 404 on classic themes, which // messes with e2e tests, so check it's a block theme first. editedPostTemplate: @@ -135,6 +138,7 @@ function EditorCanvas( { wrapperBlockName: _wrapperBlockName, wrapperUniqueId: getCurrentPostId(), deviceType: getDeviceType(), + hasHistory: !! editorSettings.goBack, }; }, [] ); const { isCleanNewPost } = useSelect( editorStore ); @@ -283,12 +287,10 @@ function EditorCanvas( { const localRef = useRef(); const typewriterRef = useTypewriter(); - const contentRef = useMergeRefs( - [ - localRef, - renderingMode === 'post-only' ? typewriterRef : undefined, - ].filter( ( r ) => !! r ) - ); + const contentRef = useMergeRefs( [ + localRef, + renderingMode === 'post-only' ? typewriterRef : noop, + ] ); return ( <BlockCanvas @@ -299,6 +301,9 @@ function EditorCanvas( { styles={ styles } height="100%" iframeProps={ { + className: classnames( 'editor-canvas__iframe', { + 'has-history': hasHistory, + } ), ...iframeProps, style: { ...iframeProps?.style, diff --git a/packages/editor/src/components/editor-canvas/style.scss b/packages/editor/src/components/editor-canvas/style.scss new file mode 100644 index 00000000000000..d5baf480124523 --- /dev/null +++ b/packages/editor/src/components/editor-canvas/style.scss @@ -0,0 +1,5 @@ +.editor-canvas__iframe { + &.has-history { + padding: $grid-unit-60 $grid-unit-60 0; + } +} diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index a47dd29fef036c..b5f3ea5b433e1c 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -209,7 +209,9 @@ export function EntitiesSavedStatesExtensible( { </Flex> <div className="entities-saved-states__text-prompt"> - <strong>{ __( 'Are you ready to save?' ) }</strong> + <strong className="entities-saved-states__text-prompt--header"> + { __( 'Are you ready to save?' ) } + </strong> { additionalPrompt } { isDirty && ( <p> diff --git a/packages/editor/src/components/entities-saved-states/style.scss b/packages/editor/src/components/entities-saved-states/style.scss index 99bb7394b510e5..8dc3d55a2bd28c 100644 --- a/packages/editor/src/components/entities-saved-states/style.scss +++ b/packages/editor/src/components/entities-saved-states/style.scss @@ -10,4 +10,8 @@ .entities-saved-states__text-prompt { padding: $grid-unit-20; padding-bottom: $grid-unit-05; + .entities-saved-states__text-prompt--header { + display: block; + margin-bottom: $grid-unit-15; + } } diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js index 127f15fbfdaf53..f9855fe8c6629f 100644 --- a/packages/editor/src/components/index.native.js +++ b/packages/editor/src/components/index.native.js @@ -9,5 +9,6 @@ export { default as EditorProvider } from './provider'; // Other Components. export { default as EditorHelpTopics } from './editor-help'; +export { default as OfflineStatus } from './offline-status'; export * from './deprecated'; diff --git a/packages/edit-post/src/components/secondary-sidebar/inserter-sidebar.js b/packages/editor/src/components/inserter-sidebar/index.js similarity index 81% rename from packages/edit-post/src/components/secondary-sidebar/inserter-sidebar.js rename to packages/editor/src/components/inserter-sidebar/index.js index 6e7ccfec7773e7..7db4335309935a 100644 --- a/packages/edit-post/src/components/secondary-sidebar/inserter-sidebar.js +++ b/packages/editor/src/components/inserter-sidebar/index.js @@ -11,21 +11,21 @@ import { } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { useEffect, useRef } from '@wordpress/element'; -import { store as editorStore } from '@wordpress/editor'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies */ -import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; export default function InserterSidebar() { const { insertionPoint, showMostUsedBlocks } = useSelect( ( select ) => { - const { isFeatureActive } = select( editPostStore ); const { getInsertionPoint } = unlock( select( editorStore ) ); + const { get } = select( preferencesStore ); return { insertionPoint: getInsertionPoint(), - showMostUsedBlocks: isFeatureActive( 'mostUsedBlocks' ), + showMostUsedBlocks: get( 'core', 'mostUsedBlocks' ), }; }, [] ); const { setIsInserterOpened } = useDispatch( editorStore ); @@ -46,16 +46,16 @@ export default function InserterSidebar() { <div ref={ inserterDialogRef } { ...inserterDialogProps } - className="edit-post-editor__inserter-panel" + className="editor-inserter-sidebar" > - <TagName className="edit-post-editor__inserter-panel-header"> + <TagName className="editor-inserter-sidebar__header"> <Button icon={ close } label={ __( 'Close block inserter' ) } onClick={ () => setIsInserterOpened( false ) } /> </TagName> - <div className="edit-post-editor__inserter-panel-content"> + <div className="editor-inserter-sidebar__content"> <Library showMostUsedBlocks={ showMostUsedBlocks } showInserterHelpPanel diff --git a/packages/editor/src/components/inserter-sidebar/style.scss b/packages/editor/src/components/inserter-sidebar/style.scss new file mode 100644 index 00000000000000..817c8e4d32814f --- /dev/null +++ b/packages/editor/src/components/inserter-sidebar/style.scss @@ -0,0 +1,22 @@ +.editor-inserter-sidebar { + @include reset; + + height: 100%; + display: flex; + flex-direction: column; +} + +.editor-inserter-sidebar__header { + padding-top: $grid-unit-10; + padding-right: $grid-unit-10; + display: flex; + justify-content: flex-end; +} + +.editor-inserter-sidebar__content { + // Leave space for the close button + height: calc(100% - #{$button-size} - #{$grid-unit-10}); + @include break-medium() { + height: 100%; + } +} diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/editor/src/components/list-view-sidebar/index.js similarity index 90% rename from packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js rename to packages/editor/src/components/list-view-sidebar/index.js index c1b4512454b150..c369dea6734971 100644 --- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/editor/src/components/list-view-sidebar/index.js @@ -11,13 +11,13 @@ import { __, _x } from '@wordpress/i18n'; import { closeSmall } from '@wordpress/icons'; import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { ESCAPE } from '@wordpress/keycodes'; -import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ import ListViewOutline from './list-view-outline'; import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; export default function ListViewSidebar() { const { setIsListViewOpened } = useDispatch( editorStore ); @@ -116,7 +116,7 @@ export default function ListViewSidebar() { function renderTabContent( tabName ) { if ( tabName === 'list-view' ) { return ( - <div className="edit-post-editor__list-view-panel-content"> + <div className="editor-list-view-sidebar__list-view-panel-content"> <ListView dropZoneElement={ dropZoneElement } /> </div> ); @@ -127,18 +127,18 @@ export default function ListViewSidebar() { return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions <div - className="edit-post-editor__document-overview-panel" + className="editor-list-view-sidebar" onKeyDown={ closeOnEscape } ref={ sidebarRef } > <Button - className="edit-post-editor__document-overview-panel__close-button" + className="editor-list-view-sidebar__close-button" icon={ closeSmall } label={ __( 'Close' ) } onClick={ closeListView } /> <TabPanel - className="edit-post-editor__document-overview-panel__tab-panel" + className="editor-list-view-sidebar__tab-panel" ref={ tabPanelRef } onSelect={ ( tabName ) => setTab( tabName ) } selectOnMove={ false } @@ -146,18 +146,18 @@ export default function ListViewSidebar() { { name: 'list-view', title: _x( 'List View', 'Post overview' ), - className: 'edit-post-sidebar__panel-tab', + className: 'editor-list-view-sidebar__panel-tab', }, { name: 'outline', title: _x( 'Outline', 'Post overview' ), - className: 'edit-post-sidebar__panel-tab', + className: 'editor-list-view-sidebar__panel-tab', }, ] } > { ( currentTab ) => ( <div - className="edit-post-editor__list-view-container" + className="editor-list-view-sidebar__list-view-container" ref={ listViewContainerRef } > { renderTabContent( currentTab.name ) } diff --git a/packages/editor/src/components/list-view-sidebar/list-view-outline.js b/packages/editor/src/components/list-view-sidebar/list-view-outline.js new file mode 100644 index 00000000000000..c73779d64f5cff --- /dev/null +++ b/packages/editor/src/components/list-view-sidebar/list-view-outline.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { __experimentalText as Text } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import CharacterCount from '../character-count'; +import WordCount from '../word-count'; +import TimeToRead from '../time-to-read'; +import DocumentOutline from '../document-outline'; + +export default function ListViewOutline() { + return ( + <> + <div className="editor-list-view-sidebar__outline"> + <div> + <Text>{ __( 'Characters:' ) }</Text> + <Text> + <CharacterCount /> + </Text> + </div> + <div> + <Text>{ __( 'Words:' ) }</Text> + <WordCount /> + </div> + <div> + <Text>{ __( 'Time to read:' ) }</Text> + <TimeToRead /> + </div> + </div> + <DocumentOutline /> + </> + ); +} diff --git a/packages/edit-post/src/components/secondary-sidebar/style.scss b/packages/editor/src/components/list-view-sidebar/style.scss similarity index 59% rename from packages/edit-post/src/components/secondary-sidebar/style.scss rename to packages/editor/src/components/list-view-sidebar/style.scss index 122d2ec3c9c525..29cc4778e3d4ab 100644 --- a/packages/edit-post/src/components/secondary-sidebar/style.scss +++ b/packages/editor/src/components/list-view-sidebar/style.scss @@ -1,23 +1,15 @@ -/** - * Note that this CSS file should be in sync with its counterpart in the other editor: - * packages/edit-site/src/components/secondary-sidebar/style.scss - */ - -.edit-post-editor__inserter-panel, -.edit-post-editor__document-overview-panel { +.editor-list-view-sidebar { height: 100%; display: flex; flex-direction: column; -} -.edit-post-editor__document-overview-panel { @include break-medium() { // Same width as the Inserter. // @see packages/block-editor/src/components/inserter/style.scss width: 350px; } - .edit-post-editor__document-overview-panel__close-button { + .editor-list-view-sidebar__close-button { position: absolute; right: $grid-unit-10; top: math.div($grid-unit-60 - $button-size, 2); // ( tab height - button size ) / 2 @@ -33,7 +25,7 @@ width: 100%; padding-right: $grid-unit-70; - .edit-post-sidebar__panel-tab { + .editor-list-view-sidebar__panel-tab { width: 50%; margin-bottom: -$border-width; } @@ -44,24 +36,8 @@ } } -.edit-post-editor__inserter-panel-header { - padding-top: $grid-unit-10; - padding-right: $grid-unit-10; - display: flex; - justify-content: flex-end; -} - -.edit-post-editor__inserter-panel-content { - // Leave space for the close button - height: calc(100% - #{$button-size} - #{$grid-unit-10}); - @include break-medium() { - height: 100%; - } -} - -.edit-post-editor__list-view-panel-content, -.edit-post-editor__list-view-container > .document-outline, -.edit-post-editor__list-view-empty-headings { +.editor-list-view-sidebar__list-view-panel-content, +.editor-list-view-sidebar__list-view-container > .document-outline { height: 100%; // Include custom scrollbars, invisible until hovered. @@ -77,19 +53,17 @@ padding: $grid-unit-10 ($grid-unit-10 - $border-width - $border-width); } -.edit-post-editor__list-view-empty-headings { - & > svg { - margin-top: $grid-unit-30 + $grid-unit-05; - } - & > p { - padding-left: $grid-unit-40; - padding-right: $grid-unit-40; - } - text-align: center; - color: $gray-700; +.editor-list-view-sidebar__list-view-container { + display: flex; + flex-direction: column; + height: 100%; +} + +.editor-list-view-sidebar__tab-panel { + height: 100%; } -.edit-post-editor__list-view-overview { +.editor-list-view-sidebar__outline { display: flex; flex-direction: column; gap: $grid-unit-10; @@ -108,13 +82,3 @@ color: $gray-700; } } - -.edit-post-editor__list-view-container { - display: flex; - flex-direction: column; - height: 100%; -} - -.edit-post-editor__document-overview-panel__tab-panel { - height: 100%; -} diff --git a/packages/editor/src/components/offline-status/index.native.js b/packages/editor/src/components/offline-status/index.native.js new file mode 100644 index 00000000000000..b136fdae1d5b29 --- /dev/null +++ b/packages/editor/src/components/offline-status/index.native.js @@ -0,0 +1,101 @@ +/** + * External dependencies + */ +import { AccessibilityInfo, Text, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { + usePreferredColorSchemeStyle, + useNetworkConnectivity, + usePrevious, +} from '@wordpress/compose'; +import { Icon } from '@wordpress/components'; +import { offline as offlineIcon } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import styles from './style.native.scss'; + +/** + * Conditionally announces messages for screen reader users. This Hook provides + * two benefits over React Native's `accessibilityLiveRegion`: + * + * 1. It works on both iOS and Android. + * 2. It allows announcing a secondary message when the component is inactive. + * + * @param {string} message The message to announce. + * @param {Object} options Options for the Hook. + * @param {boolean} [options.isActive] Whether the message should be announced. + * @param {string} [options.inactiveMessage] The message to announce when inactive. + */ +function useAccessibilityLiveRegion( message, { isActive, inactiveMessage } ) { + const { announceForAccessibility } = AccessibilityInfo; + const prevIsActive = usePrevious( isActive ); + + useEffect( () => { + const unconditionalMessage = typeof isActive === 'undefined'; + const initialRender = typeof prevIsActive === 'undefined'; + + if ( + unconditionalMessage || + ( isActive && ! prevIsActive && ! initialRender ) + ) { + announceForAccessibility( message ); + } else if ( ! isActive && prevIsActive && inactiveMessage ) { + announceForAccessibility( inactiveMessage ); + } + }, [ + message, + isActive, + prevIsActive, + inactiveMessage, + announceForAccessibility, + ] ); +} + +const OfflineStatus = () => { + const { isConnected } = useNetworkConnectivity(); + + useAccessibilityLiveRegion( __( 'Network connection re-established' ), { + isActive: isConnected, + inactiveMessage: __( 'Network connection lost, working offline' ), + } ); + + const containerStyle = usePreferredColorSchemeStyle( + styles.offline, + styles.offline__dark + ); + + const textStyle = usePreferredColorSchemeStyle( + styles[ 'offline--text' ], + styles[ 'offline--text__dark' ] + ); + + const iconStyle = usePreferredColorSchemeStyle( + styles[ 'offline--icon' ], + styles[ 'offline--icon__dark' ] + ); + + return ! isConnected ? ( + <View + accessible + accessibilityRole="alert" + accessibilityLabel={ __( + 'Network connection lost, working offline' + ) } + style={ containerStyle } + > + <View style={ containerStyle }> + <Icon fill={ iconStyle.fill } icon={ offlineIcon } /> + <Text style={ textStyle }>{ __( 'Working Offline' ) }</Text> + </View> + </View> + ) : null; +}; + +export default OfflineStatus; diff --git a/packages/block-editor/src/components/offline-status/style.native.scss b/packages/editor/src/components/offline-status/style.native.scss similarity index 100% rename from packages/block-editor/src/components/offline-status/style.native.scss rename to packages/editor/src/components/offline-status/style.native.scss diff --git a/packages/editor/src/components/offline-status/test/index.native.js b/packages/editor/src/components/offline-status/test/index.native.js new file mode 100644 index 00000000000000..7e8787c4033b01 --- /dev/null +++ b/packages/editor/src/components/offline-status/test/index.native.js @@ -0,0 +1,108 @@ +/** + * External dependencies + */ +import { act, render, screen } from 'test/helpers'; + +/** + * WordPress dependencies + */ +import { + requestConnectionStatus, + subscribeConnectionStatus, +} from '@wordpress/react-native-bridge'; + +/** + * Internal dependencies + */ +import OfflineStatus from '../index'; +import { AccessibilityInfo } from 'react-native'; + +jest.mock( '../style.native.scss', () => ( { + 'offline--icon': { + fill: '', + }, +} ) ); + +describe( 'when network connectivity is unavailable', () => { + beforeAll( () => { + requestConnectionStatus.mockImplementation( ( callback ) => { + callback( false ); + return { remove: jest.fn() }; + } ); + } ); + + it( 'should display a helpful message', () => { + render( <OfflineStatus /> ); + + expect( screen.getByText( 'Working Offline' ) ).toBeVisible(); + } ); + + it( 'should display an accessible message', () => { + render( <OfflineStatus /> ); + + expect( + screen.getByLabelText( 'Network connection lost, working offline' ) + ).toBeVisible(); + } ); + + it( 'should announce network status', () => { + render( <OfflineStatus /> ); + + expect( + AccessibilityInfo.announceForAccessibility + ).toHaveBeenCalledWith( 'Network connection lost, working offline' ); + } ); + + it( 'should announce changes to network status', () => { + let subscriptionCallback; + subscribeConnectionStatus.mockImplementation( ( callback ) => { + subscriptionCallback = callback; + return { remove: jest.fn() }; + } ); + render( <OfflineStatus /> ); + + act( () => subscriptionCallback( { isConnected: false } ) ); + + expect( + AccessibilityInfo.announceForAccessibility + ).toHaveBeenCalledWith( 'Network connection lost, working offline' ); + } ); +} ); + +describe( 'when network connectivity is available', () => { + beforeAll( () => { + requestConnectionStatus.mockImplementation( ( callback ) => { + callback( true ); + return { remove: jest.fn() }; + } ); + } ); + + it( 'should not display a helpful message', () => { + render( <OfflineStatus /> ); + + expect( screen.queryByText( 'Working Offline' ) ).toBeNull(); + } ); + + it( 'should not announce network status', () => { + render( <OfflineStatus /> ); + + expect( + AccessibilityInfo.announceForAccessibility + ).not.toHaveBeenCalled(); + } ); + + it( 'should announce changes to network status', () => { + let subscriptionCallback; + subscribeConnectionStatus.mockImplementation( ( callback ) => { + subscriptionCallback = callback; + return { remove: jest.fn() }; + } ); + render( <OfflineStatus /> ); + + act( () => subscriptionCallback( { isConnected: false } ) ); + + expect( + AccessibilityInfo.announceForAccessibility + ).toHaveBeenCalledWith( 'Network connection lost, working offline' ); + } ); +} ); diff --git a/packages/editor/src/components/post-featured-image/index.js b/packages/editor/src/components/post-featured-image/index.js index 96eb164696228f..a04701abd28095 100644 --- a/packages/editor/src/components/post-featured-image/index.js +++ b/packages/editor/src/components/post-featured-image/index.js @@ -98,17 +98,15 @@ function PostFeaturedImage( { } ) { const toggleRef = useRef(); const [ isLoading, setIsLoading ] = useState( false ); - const mediaUpload = useSelect( ( select ) => { - return select( blockEditorStore ).getSettings().mediaUpload; - }, [] ); + const { getSettings } = useSelect( blockEditorStore ); const { mediaWidth, mediaHeight, mediaSourceUrl } = getMediaDetails( media, currentPostId ); function onDropFiles( filesList ) { - mediaUpload( { - allowedTypes: [ 'image' ], + getSettings().mediaUpload( { + allowedTypes: ALLOWED_MEDIA_TYPES, filesList, onFileChange( [ image ] ) { if ( isBlobURL( image?.url ) ) { @@ -208,8 +206,6 @@ function PostFeaturedImage( { <Button className="editor-post-featured-image__action" onClick={ open } - // Prefer that screen readers use the .editor-post-featured-image__preview button. - aria-hidden="true" > { __( 'Replace' ) } </Button> diff --git a/packages/editor/src/components/post-saved-state/index.js b/packages/editor/src/components/post-saved-state/index.js index 9cccb4fcb120ec..c3089057757d9d 100644 --- a/packages/editor/src/components/post-saved-state/index.js +++ b/packages/editor/src/components/post-saved-state/index.js @@ -17,6 +17,7 @@ import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Icon, check, cloud, cloudUpload } from '@wordpress/icons'; import { displayShortcut } from '@wordpress/keycodes'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -27,16 +28,12 @@ import { store as editorStore } from '../../store'; * Component showing whether the post is saved or not and providing save * buttons. * - * @param {Object} props Component props. - * @param {?boolean} props.forceIsDirty Whether to force the post to be marked - * as dirty. - * @param {?boolean} props.showIconLabels Whether interface buttons show labels instead of icons + * @param {Object} props Component props. + * @param {?boolean} props.forceIsDirty Whether to force the post to be marked + * as dirty. * @return {import('react').ComponentType} The component. */ -export default function PostSavedState( { - forceIsDirty, - showIconLabels = false, -} ) { +export default function PostSavedState( { forceIsDirty } ) { const [ forceSavedMessage, setForceSavedMessage ] = useState( false ); const isLargeViewport = useViewportMatch( 'small' ); @@ -50,6 +47,7 @@ export default function PostSavedState( { isSaving, isScheduled, hasPublishAction, + showIconLabels, } = useSelect( ( select ) => { const { @@ -63,6 +61,7 @@ export default function PostSavedState( { isAutosavingPost, getEditedPostAttribute, } = select( editorStore ); + const { get } = select( preferencesStore ); return { isAutosaving: isAutosavingPost(), @@ -75,6 +74,7 @@ export default function PostSavedState( { isScheduled: isCurrentPostScheduled(), hasPublishAction: getCurrentPost()?._links?.[ 'wp:action-publish' ] ?? false, + showIconLabels: get( 'core', 'showIconLabels' ), }; }, [ forceIsDirty ] diff --git a/packages/editor/src/components/post-template/block-theme.js b/packages/editor/src/components/post-template/block-theme.js index a8b07cdacb554e..dcd269af1a4315 100644 --- a/packages/editor/src/components/post-template/block-theme.js +++ b/packages/editor/src/components/post-template/block-theme.js @@ -90,7 +90,8 @@ export default function BlockThemeControl( { id } ) { <MenuGroup> <MenuItem icon={ ! isTemplateHidden ? check : undefined } - isPressed={ ! isTemplateHidden } + isSelected={ ! isTemplateHidden } + role="menuitemcheckbox" onClick={ () => { setRenderingMode( isTemplateHidden diff --git a/packages/editor/src/components/post-view-link/index.js b/packages/editor/src/components/post-view-link/index.js index 57866488ff103b..fdf5f775eca456 100644 --- a/packages/editor/src/components/post-view-link/index.js +++ b/packages/editor/src/components/post-view-link/index.js @@ -6,28 +6,29 @@ import { Button } from '@wordpress/components'; import { external } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies */ import { store as editorStore } from '../../store'; -export default function PostViewLink( { showIconLabels } ) { - const { hasLoaded, permalink, isPublished, label } = useSelect( - ( select ) => { +export default function PostViewLink() { + const { hasLoaded, permalink, isPublished, label, showIconLabels } = + useSelect( ( select ) => { // Grab post type to retrieve the view_item label. const postTypeSlug = select( editorStore ).getCurrentPostType(); const postType = select( coreStore ).getPostType( postTypeSlug ); + const { get } = select( preferencesStore ); return { permalink: select( editorStore ).getPermalink(), isPublished: select( editorStore ).isCurrentPostPublished(), label: postType?.labels.view_item, hasLoaded: !! postType, + showIconLabels: get( 'core', 'showIconLabels' ), }; - }, - [] - ); + }, [] ); // Only render the view button if the post is published and has a permalink. if ( ! isPublished || ! permalink || ! hasLoaded ) { diff --git a/packages/editor/src/components/post-visibility/check.js b/packages/editor/src/components/post-visibility/check.js index 4bf9bd03772da6..116db0f546de2b 100644 --- a/packages/editor/src/components/post-visibility/check.js +++ b/packages/editor/src/components/post-visibility/check.js @@ -1,26 +1,21 @@ /** * WordPress dependencies */ -import { compose } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import { store as editorStore } from '../../store'; -export function PostVisibilityCheck( { hasPublishAction, render } ) { - const canEdit = hasPublishAction; +export default function PostVisibilityCheck( { render } ) { + const canEdit = useSelect( ( select ) => { + return ( + select( editorStore ).getCurrentPost()._links?.[ + 'wp:action-publish' + ] ?? false + ); + } ); + return render( { canEdit } ); } - -export default compose( [ - withSelect( ( select ) => { - const { getCurrentPost, getCurrentPostType } = select( editorStore ); - return { - hasPublishAction: - getCurrentPost()._links?.[ 'wp:action-publish' ] ?? false, - postType: getCurrentPostType(), - }; - } ), -] )( PostVisibilityCheck ); diff --git a/packages/editor/src/components/post-visibility/test/check.js b/packages/editor/src/components/post-visibility/test/check.js index 8ec0c2df04ec90..828e876cceb102 100644 --- a/packages/editor/src/components/post-visibility/test/check.js +++ b/packages/editor/src/components/post-visibility/test/check.js @@ -3,32 +3,43 @@ */ import { render, screen } from '@testing-library/react'; +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); + /** * Internal dependencies */ -import { PostVisibilityCheck } from '../check'; +import PostVisibilityCheck from '../check'; + +function setupMockSelect( hasPublishAction ) { + useSelect.mockImplementation( ( mapSelect ) => { + return mapSelect( () => ( { + getCurrentPost: () => ( { + _links: { + 'wp:action-publish': hasPublishAction, + }, + } ), + } ) ); + } ); +} describe( 'PostVisibilityCheck', () => { const renderProp = ( { canEdit } ) => ( canEdit ? 'yes' : 'no' ); it( "should not render the edit link if the user doesn't have the right capability", () => { - render( - <PostVisibilityCheck - hasPublishAction={ false } - render={ renderProp } - /> - ); + setupMockSelect( false ); + render( <PostVisibilityCheck render={ renderProp } /> ); expect( screen.queryByText( 'yes' ) ).not.toBeInTheDocument(); expect( screen.getByText( 'no' ) ).toBeVisible(); } ); it( 'should render if the user has the correct capability', () => { - render( - <PostVisibilityCheck - hasPublishAction={ true } - render={ renderProp } - /> - ); + setupMockSelect( true ); + render( <PostVisibilityCheck render={ renderProp } /> ); expect( screen.queryByText( 'no' ) ).not.toBeInTheDocument(); expect( screen.getByText( 'yes' ) ).toBeVisible(); } ); diff --git a/packages/editor/src/components/preview-dropdown/index.js b/packages/editor/src/components/preview-dropdown/index.js index b7d64f2eeebc63..d78ef020fe232f 100644 --- a/packages/editor/src/components/preview-dropdown/index.js +++ b/packages/editor/src/components/preview-dropdown/index.js @@ -13,6 +13,7 @@ import { __ } from '@wordpress/i18n'; import { check, desktop, mobile, tablet, external } from '@wordpress/icons'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -20,25 +21,21 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as editorStore } from '../../store'; import PostPreviewButton from '../post-preview-button'; -export default function PreviewDropdown( { - showIconLabels, - forceIsAutosaveable, - disabled, -} ) { - const { deviceType, homeUrl, isTemplate, isViewable } = useSelect( - ( select ) => { +export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { + const { deviceType, homeUrl, isTemplate, isViewable, showIconLabels } = + useSelect( ( select ) => { const { getDeviceType, getCurrentPostType } = select( editorStore ); const { getUnstableBase, getPostType } = select( coreStore ); + const { get } = select( preferencesStore ); const _currentPostType = getCurrentPostType(); return { deviceType: getDeviceType(), homeUrl: getUnstableBase()?.home, isTemplate: _currentPostType === 'wp_template', isViewable: getPostType( _currentPostType )?.viewable ?? false, + showIconLabels: get( 'core', 'showIconLabels' ), }; - }, - [] - ); + }, [] ); const { setDeviceType } = useDispatch( editorStore ); const isMobile = useViewportMatch( 'medium', '<' ); if ( isMobile ) return null; diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index bbd710f031c849..17a7a55bfedd43 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -1,6 +1,7 @@ /** * External dependencies */ +import { BackHandler } from 'react-native'; import memize from 'memize'; import { SafeAreaProvider } from 'react-native-safe-area-context'; @@ -79,6 +80,8 @@ class NativeEditorProvider extends Component { this.post ); + this.onHardwareBackPress = this.onHardwareBackPress.bind( this ); + this.getEditorSettings = memize( ( settings, capabilities ) => ( { ...settings, @@ -191,6 +194,11 @@ class NativeEditorProvider extends Component { this.setState( { isHelpVisible: true } ); } ); + this.hardwareBackPressListener = BackHandler.addEventListener( + 'hardwareBackPress', + this.onHardwareBackPress + ); + // Request current block impressions from native app. requestBlockTypeImpressions( ( storedImpressions ) => { const impressions = { ...NEW_BLOCK_TYPES, ...storedImpressions }; @@ -250,6 +258,10 @@ class NativeEditorProvider extends Component { if ( this.subscriptionParentShowEditorHelp ) { this.subscriptionParentShowEditorHelp.remove(); } + + if ( this.hardwareBackPressListener ) { + this.hardwareBackPressListener.remove(); + } } getThemeColors( { rawStyles, rawFeatures } ) { @@ -280,6 +292,16 @@ class NativeEditorProvider extends Component { } } + onHardwareBackPress() { + const { clearSelectedBlock, selectedBlockIndex } = this.props; + + if ( selectedBlockIndex !== -1 ) { + clearSelectedBlock(); + return true; + } + return false; + } + serializeToNativeAction() { const title = this.props.title; let html; @@ -397,8 +419,12 @@ const ComposedNativeProvider = compose( [ withDispatch( ( dispatch ) => { const { editPost, resetEditorBlocks, updateEditorSettings } = dispatch( editorStore ); - const { updateSettings, insertBlock, replaceBlock } = - dispatch( blockEditorStore ); + const { + clearSelectedBlock, + updateSettings, + insertBlock, + replaceBlock, + } = dispatch( blockEditorStore ); const { switchEditorMode } = dispatch( editPostStore ); const { addEntities, receiveEntityRecords } = dispatch( coreStore ); const { createSuccessNotice, createErrorNotice } = @@ -411,6 +437,7 @@ const ComposedNativeProvider = compose( [ insertBlock, createSuccessNotice, createErrorNotice, + clearSelectedBlock, editTitle( title ) { editPost( { title } ); }, diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 49f61815f663d0..eddc295766f8ce 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -9,6 +9,8 @@ import { __experimentalFetchUrlData as fetchUrlData, } from '@wordpress/core-data'; import { __ } from '@wordpress/i18n'; +import { store as preferencesStore } from '@wordpress/preferences'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -28,7 +30,6 @@ const BLOCK_EDITOR_SETTINGS = [ '__unstableGalleryWithImageBlocks', 'alignWide', 'allowedBlockTypes', - 'allowRightClickOverrides', 'blockInspectorTabs', 'allowedMimeTypes', 'bodyPlaceholder', @@ -46,20 +47,16 @@ const BLOCK_EDITOR_SETTINGS = [ 'enableCustomSpacing', 'enableCustomUnits', 'enableOpenverseMediaCategory', - 'focusMode', - 'distractionFree', 'fontSizes', 'gradients', 'generateAnchors', - 'hasFixedToolbar', + 'getPostLinkProps', 'hasInlineToolbar', - 'isDistractionFree', 'imageDefaultSize', 'imageDimensions', 'imageEditing', 'imageSizes', 'isRTL', - 'keepCaretInsideBlock', 'locale', 'maxWidth', 'onUpdateDefaultBlockStyles', @@ -88,7 +85,13 @@ const BLOCK_EDITOR_SETTINGS = [ * @return {Object} Block Editor Settings. */ function useBlockEditorSettings( settings, postType, postId ) { + const isLargeViewport = useViewportMatch( 'medium' ); const { + allowRightClickOverrides, + focusMode, + hasFixedToolbar, + isDistractionFree, + keepCaretInsideBlock, reusableBlocks, hasUploadPermissions, canUseUnfilteredHTML, @@ -110,17 +113,27 @@ function useBlockEditorSettings( settings, postType, postId ) { getBlockPatterns, getBlockPatternCategories, } = select( coreStore ); + const { get } = select( preferencesStore ); const siteSettings = canUser( 'read', 'settings' ) ? getEntityRecord( 'root', 'site' ) : undefined; return { + allowRightClickOverrides: get( + 'core', + 'allowRightClickOverrides' + ), canUseUnfilteredHTML: getRawEntityRecord( 'postType', postType, postId )?._links?.hasOwnProperty( 'wp:action-unfiltered-html' ), + focusMode: get( 'core', 'focusMode' ), + hasFixedToolbar: + get( 'core', 'fixedToolbar' ) || ! isLargeViewport, + isDistractionFree: get( 'core', 'distractionFree' ), + keepCaretInsideBlock: get( 'core', 'keepCaretInsideBlock' ), reusableBlocks: isWeb ? getEntityRecords( 'postType', 'wp_block', { per_page: -1, @@ -135,7 +148,7 @@ function useBlockEditorSettings( settings, postType, postId ) { restBlockPatternCategories: getBlockPatternCategories(), }; }, - [ postType, postId ] + [ postType, postId, isLargeViewport ] ); const settingsBlockPatterns = @@ -202,6 +215,8 @@ function useBlockEditorSettings( settings, postType, postId ) { [ saveEntityRecord, userCanCreatePages ] ); + const forceDisableFocusMode = settings.focusMode === false; + return useMemo( () => ( { ...Object.fromEntries( @@ -209,6 +224,11 @@ function useBlockEditorSettings( settings, postType, postId ) { BLOCK_EDITOR_SETTINGS.includes( key ) ) ), + allowRightClickOverrides, + focusMode: focusMode && ! forceDisableFocusMode, + hasFixedToolbar, + isDistractionFree, + keepCaretInsideBlock, mediaUpload: hasUploadPermissions ? mediaUpload : undefined, __experimentalReusableBlocks: reusableBlocks, __experimentalBlockPatterns: blockPatterns, @@ -241,6 +261,12 @@ function useBlockEditorSettings( settings, postType, postId ) { __experimentalSetIsInserterOpened: setIsInserterOpened, } ), [ + allowRightClickOverrides, + focusMode, + forceDisableFocusMode, + hasFixedToolbar, + isDistractionFree, + keepCaretInsideBlock, settings, hasUploadPermissions, reusableBlocks, diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index fab84cdd53946c..5f8fc7ccf73185 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -6,6 +6,9 @@ import { ExperimentalEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; import { EntitiesSavedStatesExtensible } from './components/entities-saved-states'; import useBlockEditorSettings from './components/provider/use-block-editor-settings'; +import DocumentTools from './components/document-tools'; +import InserterSidebar from './components/inserter-sidebar'; +import ListViewSidebar from './components/list-view-sidebar'; import PostPanelRow from './components/post-panel-row'; import PostViewLink from './components/post-view-link'; import PreviewDropdown from './components/preview-dropdown'; @@ -13,9 +16,12 @@ import PluginPostExcerpt from './components/post-excerpt/plugin'; export const privateApis = {}; lock( privateApis, { + DocumentTools, EditorCanvas, ExperimentalEditorProvider, EntitiesSavedStatesExtensible, + InserterSidebar, + ListViewSidebar, PostPanelRow, PostViewLink, PreviewDropdown, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 49f49a5d06da5d..a0330321bac8f7 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -624,7 +624,7 @@ export const toggleEditorPanelEnabled = const inactivePanels = registry .select( preferencesStore ) - .get( 'core/edit-post', 'inactivePanels' ) ?? []; + .get( 'core', 'inactivePanels' ) ?? []; const isPanelInactive = !! inactivePanels?.includes( panelName ); @@ -641,7 +641,7 @@ export const toggleEditorPanelEnabled = registry .dispatch( preferencesStore ) - .set( 'core/edit-post', 'inactivePanels', updatedInactivePanels ); + .set( 'core', 'inactivePanels', updatedInactivePanels ); }; /** @@ -653,9 +653,8 @@ export const toggleEditorPanelOpened = ( panelName ) => ( { registry } ) => { const openPanels = - registry - .select( preferencesStore ) - .get( 'core/edit-post', 'openPanels' ) ?? []; + registry.select( preferencesStore ).get( 'core', 'openPanels' ) ?? + []; const isPanelOpen = !! openPanels?.includes( panelName ); @@ -672,7 +671,7 @@ export const toggleEditorPanelOpened = registry .dispatch( preferencesStore ) - .set( 'core/edit-post', 'openPanels', updatedOpenPanels ); + .set( 'core', 'openPanels', updatedOpenPanels ); }; /** diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js index b05dcae93c9472..e276859f884038 100644 --- a/packages/editor/src/store/private-selectors.js +++ b/packages/editor/src/store/private-selectors.js @@ -1,14 +1,8 @@ -/** - * External dependencies - */ -import createSelector from 'rememo'; - /** * WordPress dependencies */ import { store as blockEditorStore } from '@wordpress/block-editor'; import { createRegistrySelector } from '@wordpress/data'; -import { createRef } from '@wordpress/element'; /** * Internal dependencies @@ -52,9 +46,6 @@ export const getInsertionPoint = createRegistrySelector( } ); -export const getListViewToggleRef = createSelector( - () => { - return createRef(); - }, - () => [] -); +export function getListViewToggleRef( state ) { + return state.listViewToggleRef; +} diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index a2d24789cd33f5..978a5c8697410a 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -349,6 +349,17 @@ export function listViewPanel( state = false, action ) { return state; } +/** + * This reducer does nothing aside initializing a ref to the list view toggle. + * We will have a unique ref per "editor" instance. + * + * @param {Object} state + * @return {Object} Reference to the list view toggle button. + */ +export function listViewToggleRef( state = { current: null } ) { + return state; +} + export default combineReducers( { postId, postType, @@ -365,4 +376,5 @@ export default combineReducers( { removedPanels, blockInserterPanel, listViewPanel, + listViewToggleRef, } ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 70d726638a0940..107ffe4dd4625c 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1152,7 +1152,7 @@ export const isEditorPanelEnabled = createRegistrySelector( // For backward compatibility, we check edit-post // even though now this is in "editor" package. const inactivePanels = select( preferencesStore ).get( - 'core/edit-post', + 'core', 'inactivePanels' ); return ( @@ -1176,7 +1176,7 @@ export const isEditorPanelOpened = createRegistrySelector( // For backward compatibility, we check edit-post // even though now this is in "editor" package. const openPanels = select( preferencesStore ).get( - 'core/edit-post', + 'core', 'openPanels' ); return !! openPanels?.includes( panelName ); diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 50359984af1628..ff5a55a3881f99 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -1,9 +1,12 @@ @import "./components/autocompleters/style.scss"; @import "./components/document-bar/style.scss"; @import "./components/document-outline/style.scss"; +@import "./components/document-tools/style.scss"; @import "./components/editor-notices/style.scss"; @import "./components/entities-saved-states/style.scss"; @import "./components/error-boundary/style.scss"; +@import "./components/inserter-sidebar/style.scss"; +@import "./components/list-view-sidebar/style.scss"; @import "./components/post-author/style.scss"; @import "./components/post-excerpt/style.scss"; @import "./components/post-featured-image/style.scss"; @@ -26,3 +29,4 @@ @import "./components/preview-dropdown/style.scss"; @import "./components/table-of-contents/style.scss"; @import "./components/template-validation-notice/style.scss"; +@import "./components/editor-canvas/style.scss"; diff --git a/packages/editor/src/utils/media-upload/index.js b/packages/editor/src/utils/media-upload/index.js index 3edd4fec51d4be..f1f60b7c1e99a0 100644 --- a/packages/editor/src/utils/media-upload/index.js +++ b/packages/editor/src/utils/media-upload/index.js @@ -31,17 +31,24 @@ export default function mediaUpload( { onError = noop, onFileChange, } ) { - const { getCurrentPostId, getEditorSettings } = select( editorStore ); + const { getCurrentPost, getEditorSettings } = select( editorStore ); const wpAllowedMimeTypes = getEditorSettings().allowedMimeTypes; maxUploadFileSize = maxUploadFileSize || getEditorSettings().maxUploadFileSize; - const currentPostId = getCurrentPostId(); + const currentPost = getCurrentPost(); + // Templates and template parts' numerical ID is stored in `wp_id`. + const currentPostId = + typeof currentPost?.id === 'number' + ? currentPost.id + : currentPost?.wp_id; + const postData = currentPostId ? { post: currentPostId } : {}; + uploadMedia( { allowedTypes, filesList, onFileChange, additionalData: { - ...( ! isNaN( currentPostId ) ? { post: currentPostId } : {} ), + ...postData, ...additionalData, }, maxUploadFileSize, diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md index 971ee2b5dbd7ef..89c422ad27a0bb 100644 --- a/packages/element/CHANGELOG.md +++ b/packages/element/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 5.26.0 (2023-12-27) - ## 5.25.0 (2023-12-13) ## 5.24.0 (2023-11-29) diff --git a/packages/element/package.json b/packages/element/package.json index 4f4318038abbbf..6ce4714c25f0bf 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/element", - "version": "5.26.0-prerelease", + "version": "5.25.0", "description": "Element React module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 055e06c8a343fc..6605add7800961 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 9.1.0 (2023-12-27) - ## 9.0.0 (2023-12-13) ### Breaking Change diff --git a/packages/env/package.json b/packages/env/package.json index 8e17750811551d..cd3fd075568e2e 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/env", - "version": "9.1.0-prerelease", + "version": "9.0.0", "description": "A zero-config, self contained local WordPress environment for development and testing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/escape-html/CHANGELOG.md b/packages/escape-html/CHANGELOG.md index 666d40e0f39209..7b5f651626528e 100644 --- a/packages/escape-html/CHANGELOG.md +++ b/packages/escape-html/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 2.49.0 (2023-12-27) - ## 2.48.0 (2023-12-13) ## 2.47.0 (2023-11-29) diff --git a/packages/escape-html/package.json b/packages/escape-html/package.json index 18c10a29b489ed..2618d5818400c6 100644 --- a/packages/escape-html/package.json +++ b/packages/escape-html/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/escape-html", - "version": "2.49.0-prerelease", + "version": "2.48.0", "description": "Escape HTML utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 65d8a916b064a3..533c5e7788c663 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 17.6.0 (2023-12-27) - ## 17.5.0 (2023-12-13) ## 17.4.0 (2023-11-29) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 18bb33042f1d99..ef0931dfb2c6c7 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/eslint-plugin", - "version": "17.6.0-prerelease", + "version": "17.5.0", "description": "ESLint plugin for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index c263869c08f9d2..8b3d314e9a0f25 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.26.0 (2023-12-27) - ## 4.25.0 (2023-12-13) ## 4.24.0 (2023-11-29) diff --git a/packages/format-library/package.json b/packages/format-library/package.json index a37b5a2a646dee..b71addc176e7c1 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "4.26.0-prerelease", + "version": "4.25.0", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -35,6 +35,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/rich-text": "file:../rich-text", "@wordpress/url": "file:../url" }, diff --git a/packages/format-library/src/lock-unlock.js b/packages/format-library/src/lock-unlock.js new file mode 100644 index 00000000000000..f7512caa4b746f --- /dev/null +++ b/packages/format-library/src/lock-unlock.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', + '@wordpress/format-library' + ); diff --git a/packages/format-library/src/text-color/inline.js b/packages/format-library/src/text-color/inline.js index f71c7766222582..98ced2cca01e29 100644 --- a/packages/format-library/src/text-color/inline.js +++ b/packages/format-library/src/text-color/inline.js @@ -17,13 +17,24 @@ import { store as blockEditorStore, useCachedTruthy, } from '@wordpress/block-editor'; -import { Popover, TabPanel } from '@wordpress/components'; +import { + Popover, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import { textColor as settings, transparentValue } from './index'; +import { unlock } from '../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); + +const TABS = [ + { name: 'color', title: __( 'Text' ) }, + { name: 'backgroundColor', title: __( 'Background' ) }, +]; function parseCSS( css = '' ) { return css.split( ';' ).reduce( ( accumulator, rule ) => { @@ -155,30 +166,32 @@ export default function InlineColorUI( { return ( <Popover onClose={ onClose } - className="components-inline-color-popover" + className="format-library__inline-color-popover" anchor={ popoverAnchor } > - <TabPanel - tabs={ [ - { - name: 'color', - title: __( 'Text' ), - }, - { - name: 'backgroundColor', - title: __( 'Background' ), - }, - ] } - > - { ( tab ) => ( - <ColorPicker - name={ name } - property={ tab.name } - value={ value } - onChange={ onChange } - /> - ) } - </TabPanel> + <Tabs> + <Tabs.TabList> + { TABS.map( ( tab ) => ( + <Tabs.Tab tabId={ tab.name } key={ tab.name }> + { tab.title } + </Tabs.Tab> + ) ) } + </Tabs.TabList> + { TABS.map( ( tab ) => ( + <Tabs.TabPanel + tabId={ tab.name } + focusable={ false } + key={ tab.name } + > + <ColorPicker + name={ name } + property={ tab.name } + value={ value } + onChange={ onChange } + /> + </Tabs.TabPanel> + ) ) } + </Tabs> </Popover> ); } diff --git a/packages/format-library/src/text-color/style.scss b/packages/format-library/src/text-color/style.scss index 121ba8da756cf5..439af6db38d0cf 100644 --- a/packages/format-library/src/text-color/style.scss +++ b/packages/format-library/src/text-color/style.scss @@ -1,23 +1,6 @@ -.components-inline-color-popover { +.format-library__inline-color-popover { - .components-popover__content { - .components-tab-panel__tab-content { - padding: 16px; - } - - .components-color-palette { - margin-top: 0.6rem; - } - - .components-base-control__title { - display: block; - margin-bottom: 16px; - font-weight: 600; - color: #191e23; - } - - .component-color-indicator { - vertical-align: text-bottom; - } + [role="tabpanel"] { + padding: 16px; } } diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md index 0d6f1faff1efb7..c428f26ed17f10 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.49.0 (2023-12-27) - ## 3.48.0 (2023-12-13) ## 3.47.0 (2023-11-29) diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 84074b3575c2db..0c29dacb178950 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/hooks", - "version": "3.49.0-prerelease", + "version": "3.48.0", "description": "WordPress hooks library.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/html-entities/CHANGELOG.md b/packages/html-entities/CHANGELOG.md index f9c858098278ca..d7c1b049e37900 100644 --- a/packages/html-entities/CHANGELOG.md +++ b/packages/html-entities/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.49.0 (2023-12-27) - ## 3.48.0 (2023-12-13) ## 3.47.0 (2023-11-29) diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index b6ae9c2634dede..92e85598707b90 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/html-entities", - "version": "3.49.0-prerelease", + "version": "3.48.0", "description": "HTML entity utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index bcf9e392fb2346..f227f81571087f 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.49.0 (2023-12-27) - ## 4.48.0 (2023-12-13) ## 4.47.0 (2023-11-29) diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 56769f3e33736c..7346330946e19d 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/i18n", - "version": "4.49.0-prerelease", + "version": "4.48.0", "description": "WordPress internationalization (i18n) library.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/icons/CHANGELOG.md b/packages/icons/CHANGELOG.md index 0e75fa8be3a6c7..df0e3e43b1d816 100644 --- a/packages/icons/CHANGELOG.md +++ b/packages/icons/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 9.40.0 (2023-12-27) - ## 9.39.0 (2023-12-13) ## 9.38.0 (2023-11-29) diff --git a/packages/icons/package.json b/packages/icons/package.json index 0c359d100a34b4..31ebc3c57d1290 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/icons", - "version": "9.40.0-prerelease", + "version": "9.39.0", "description": "WordPress Icons package, based on dashicon.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/icons/src/library/offline.js b/packages/icons/src/library/offline.js index f0daa1aaeb79ee..444d3667f297e8 100644 --- a/packages/icons/src/library/offline.js +++ b/packages/icons/src/library/offline.js @@ -4,13 +4,7 @@ import { SVG, Path } from '@wordpress/primitives'; const offline = ( - <SVG - width="16" - height="16" - viewBox="0 0 16 16" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > + <SVG viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <Path fill-rule="evenodd" clip-rule="evenodd" diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 88fd02beecb1de..8c03cfc314efed 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.2.0 (2023-12-27) - ### Bug Fix - Fix namespaces when there are nested interactive regions. ([#57029](https://github.com/WordPress/gutenberg/pull/57029)) diff --git a/packages/interactivity/docs/1-getting-started.md b/packages/interactivity/docs/1-getting-started.md index 85af2021807351..660671a8b10cd8 100644 --- a/packages/interactivity/docs/1-getting-started.md +++ b/packages/interactivity/docs/1-getting-started.md @@ -26,18 +26,6 @@ We can scaffold a WordPress plugin that registers an interactive block (using th npx @wordpress/create-block@latest my-first-interactive-block --template @wordpress/create-block-interactive-template ``` -> **Note** -> The Interactivity API recently switched from [using modules instead of scripts in the frontend](https://github.com/WordPress/gutenberg/pull/56143). Therefore, in order to test this scaffolded block, you will need to add the following line to the `package.json` file of the generated plugin: - -```json -"files": [ - "src/view.js" -] -``` -> This should be updated in the [scripts package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) soon. - - - #### 2. Generate the build When the plugin folder is generated, we should launch the build process to get the final version of the interactive block that can be used from WordPress. @@ -61,7 +49,7 @@ At this point you should be able to insert the "My First Interactive Block" bloc ## Requirements of the Interactivity API -To start working with the Interactivity API you'll need to have a [proper WordPress development environment for blocks](https://developer.wordpress.org/block-editor/getting-started/devenv/) and some specific code in your block, which should include: +To start working with the Interactivity API you'll need to have a [proper WordPress development environment for blocks](https://developer.wordpress.org/block-editor/getting-started/devenv/) and some specific code in your block, which should include: #### A local WordPress installation @@ -71,7 +59,7 @@ To get quickly started, [`wp-now`](https://www.npmjs.com/package/@wp-now/wp-now) #### Latest vesion of Gutenberg -The Interactivity API is currently only available as an experimental feature from Gutenberg 17.2, so you'll need to have Gutenberg 17.2 or higher version installed and activated in your WordPress installation. +The Interactivity API is currently only available as an experimental feature from Gutenberg, so you'll need to have Gutenberg 17.5 or higher version installed and activated in your WordPress installation. #### Node.js diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index b7ed52dd288c8a..c856628c5d79e2 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interactivity", - "version": "3.2.0-prerelease", + "version": "3.1.0", "description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index 7dcb57d9502242..90e0326e02a47e 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 5.26.0 (2023-12-27) - ## 5.25.0 (2023-12-13) ## 5.24.0 (2023-11-29) diff --git a/packages/interface/package.json b/packages/interface/package.json index c526ca990f43ae..df3d53990e0f59 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "5.26.0-prerelease", + "version": "5.25.0", "description": "Interface module for WordPress. The package contains shared functionality across the modern JavaScript-based WordPress screens.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/src/components/complementary-area/index.js b/packages/interface/src/components/complementary-area/index.js index af6456bec09663..2664997d2c7216 100644 --- a/packages/interface/src/components/complementary-area/index.js +++ b/packages/interface/src/components/complementary-area/index.js @@ -12,6 +12,7 @@ import { __ } from '@wordpress/i18n'; import { check, starEmpty, starFilled } from '@wordpress/icons'; import { useEffect, useRef } from '@wordpress/element'; import { store as viewportStore } from '@wordpress/viewport'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -108,31 +109,38 @@ function ComplementaryArea( { title, toggleShortcut, isActiveByDefault, - showIconLabels = false, } ) { - const { isLoading, isActive, isPinned, activeArea, isSmall, isLarge } = - useSelect( - ( select ) => { - const { - getActiveComplementaryArea, - isComplementaryAreaLoading, - isItemPinned, - } = select( interfaceStore ); + const { + isLoading, + isActive, + isPinned, + activeArea, + isSmall, + isLarge, + showIconLabels, + } = useSelect( + ( select ) => { + const { + getActiveComplementaryArea, + isComplementaryAreaLoading, + isItemPinned, + } = select( interfaceStore ); + const { get } = select( preferencesStore ); - const _activeArea = getActiveComplementaryArea( scope ); + const _activeArea = getActiveComplementaryArea( scope ); - return { - isLoading: isComplementaryAreaLoading( scope ), - isActive: _activeArea === identifier, - isPinned: isItemPinned( scope, identifier ), - activeArea: _activeArea, - isSmall: - select( viewportStore ).isViewportMatch( '< medium' ), - isLarge: select( viewportStore ).isViewportMatch( 'large' ), - }; - }, - [ identifier, scope ] - ); + return { + isLoading: isComplementaryAreaLoading( scope ), + isActive: _activeArea === identifier, + isPinned: isItemPinned( scope, identifier ), + activeArea: _activeArea, + isSmall: select( viewportStore ).isViewportMatch( '< medium' ), + isLarge: select( viewportStore ).isViewportMatch( 'large' ), + showIconLabels: get( 'core', 'showIconLabels' ), + }; + }, + [ identifier, scope ] + ); useAdjustComplementaryListener( scope, identifier, diff --git a/packages/interface/src/components/preferences-modal-section/index.js b/packages/interface/src/components/preferences-modal-section/index.js index ea164128ea54cb..8ea2ca2652d6df 100644 --- a/packages/interface/src/components/preferences-modal-section/index.js +++ b/packages/interface/src/components/preferences-modal-section/index.js @@ -10,7 +10,9 @@ const Section = ( { description, title, children } ) => ( </p> ) } </legend> - { children } + <div className="interface-preferences-modal__section-content"> + { children } + </div> </fieldset> ); diff --git a/packages/interface/src/components/preferences-modal-section/style.scss b/packages/interface/src/components/preferences-modal-section/style.scss index 1a45642a8b7af9..a1259af3430d56 100644 --- a/packages/interface/src/components/preferences-modal-section/style.scss +++ b/packages/interface/src/components/preferences-modal-section/style.scss @@ -22,3 +22,7 @@ font-style: normal; color: $gray-700; } + +.interface-preferences-modal__section:has(.interface-preferences-modal__section-content:empty) { + display: none; +} diff --git a/packages/interface/src/components/preferences-modal/README.md b/packages/interface/src/components/preferences-modal/README.md index f873ccf297ec12..4327a59a7905ae 100644 --- a/packages/interface/src/components/preferences-modal/README.md +++ b/packages/interface/src/components/preferences-modal/README.md @@ -28,7 +28,7 @@ function MyEditorPreferencesModal() { 'Review settings, such as visibility and tags.' ) } label={ __( - 'Enable pre-publish flow' + 'Enable pre-publish checks' ) } /> </PreferencesModalSection> diff --git a/packages/is-shallow-equal/CHANGELOG.md b/packages/is-shallow-equal/CHANGELOG.md index 89e6b0506de959..46ad3508ad0416 100644 --- a/packages/is-shallow-equal/CHANGELOG.md +++ b/packages/is-shallow-equal/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.49.0 (2023-12-27) - ## 4.48.0 (2023-12-13) ## 4.47.0 (2023-11-29) diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index d37a0d1a735c4f..6969d6a2fe8a05 100644 --- a/packages/is-shallow-equal/package.json +++ b/packages/is-shallow-equal/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/is-shallow-equal", - "version": "4.49.0-prerelease", + "version": "4.48.0", "description": "Test for shallow equality between two objects or arrays.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-console/CHANGELOG.md b/packages/jest-console/CHANGELOG.md index fe28340878df67..bf9aba56a98e12 100644 --- a/packages/jest-console/CHANGELOG.md +++ b/packages/jest-console/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 7.20.0 (2023-12-27) - ## 7.19.0 (2023-12-13) ## 7.18.0 (2023-11-29) diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index aec655cdef0253..4a6f249a104d27 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-console", - "version": "7.20.0-prerelease", + "version": "7.19.0", "description": "Custom Jest matchers for the Console object.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index e4d36bd6767b7f..5b945545507814 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 11.20.0 (2023-12-27) - ## 11.19.0 (2023-12-13) ## 11.18.0 (2023-11-29) diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index 0eea603b2ad17c..7bda9f2e279f25 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-preset-default", - "version": "11.20.0-prerelease", + "version": "11.19.0", "description": "Default Jest preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-puppeteer-axe/CHANGELOG.md b/packages/jest-puppeteer-axe/CHANGELOG.md index 1943f5f6e60879..9114f0e0437727 100644 --- a/packages/jest-puppeteer-axe/CHANGELOG.md +++ b/packages/jest-puppeteer-axe/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 6.20.0 (2023-12-27) - ## 6.19.0 (2023-12-13) ## 6.18.0 (2023-11-29) diff --git a/packages/jest-puppeteer-axe/package.json b/packages/jest-puppeteer-axe/package.json index 44f86cd158e7e9..cdd09f7b791654 100644 --- a/packages/jest-puppeteer-axe/package.json +++ b/packages/jest-puppeteer-axe/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.20.0-prerelease", + "version": "6.19.0", "description": "Axe API integration with Jest and Puppeteer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keyboard-shortcuts/CHANGELOG.md b/packages/keyboard-shortcuts/CHANGELOG.md index 5cc67c021a4a5f..32175acb451f57 100644 --- a/packages/keyboard-shortcuts/CHANGELOG.md +++ b/packages/keyboard-shortcuts/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.26.0 (2023-12-27) - ## 4.25.0 (2023-12-13) ## 4.24.0 (2023-11-29) diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index 8e1c2b35130fd1..af36a49371a0be 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keyboard-shortcuts", - "version": "4.26.0-prerelease", + "version": "4.25.0", "description": "Handling keyboard shortcuts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keycodes/CHANGELOG.md b/packages/keycodes/CHANGELOG.md index 4dad05bd4ef331..b65e8f808d595a 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.49.0 (2023-12-27) - ## 3.48.0 (2023-12-13) ## 3.47.0 (2023-11-29) diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 342d50ba8e6e0f..ecebbb58eb99f4 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "3.49.0-prerelease", + "version": "3.48.0", "description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/lazy-import/CHANGELOG.md b/packages/lazy-import/CHANGELOG.md index 10e5165ce95dbd..87ebbe936eaa4c 100644 --- a/packages/lazy-import/CHANGELOG.md +++ b/packages/lazy-import/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 1.36.0 (2023-12-27) - ## 1.35.0 (2023-12-13) ## 1.34.0 (2023-11-29) diff --git a/packages/lazy-import/package.json b/packages/lazy-import/package.json index 644121850eab42..b1967c0d4bedb0 100644 --- a/packages/lazy-import/package.json +++ b/packages/lazy-import/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/lazy-import", - "version": "1.36.0-prerelease", + "version": "1.35.0", "description": "Lazily import a module, installing it automatically if missing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index a652ab7ff99dca..d1dd89a70ad630 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.26.0 (2023-12-27) - ## 4.25.0 (2023-12-13) ## 4.24.0 (2023-11-29) diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 1decde84386eb7..bba7e2a7680e59 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/list-reusable-blocks", - "version": "4.26.0-prerelease", + "version": "4.25.0", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/media-utils/CHANGELOG.md b/packages/media-utils/CHANGELOG.md index dc13dd12984fdf..8d8a13b4ae6ab8 100644 --- a/packages/media-utils/CHANGELOG.md +++ b/packages/media-utils/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.40.0 (2023-12-27) - ## 4.39.0 (2023-12-13) ## 4.38.0 (2023-11-29) diff --git a/packages/media-utils/package.json b/packages/media-utils/package.json index 3babc2af8c07e6..1086a79722cb06 100644 --- a/packages/media-utils/package.json +++ b/packages/media-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/media-utils", - "version": "4.40.0-prerelease", + "version": "4.39.0", "description": "WordPress Media Upload Utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index 0060820125e00d..0b4c95da740056 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.17.0 (2023-12-27) - ## 4.16.0 (2023-12-13) ## 4.15.0 (2023-11-29) diff --git a/packages/notices/package.json b/packages/notices/package.json index eb7ea82e973e49..7c40413adea511 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "4.17.0-prerelease", + "version": "4.16.0", "description": "State management for notices.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/npm-package-json-lint-config/CHANGELOG.md b/packages/npm-package-json-lint-config/CHANGELOG.md index 7278e305970809..2eaaf4c0df1e92 100644 --- a/packages/npm-package-json-lint-config/CHANGELOG.md +++ b/packages/npm-package-json-lint-config/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.34.0 (2023-12-27) - ## 4.33.0 (2023-12-13) ## 4.32.0 (2023-11-29) diff --git a/packages/npm-package-json-lint-config/package.json b/packages/npm-package-json-lint-config/package.json index 2f16d73f738b43..3bbc4c6c2e47fd 100644 --- a/packages/npm-package-json-lint-config/package.json +++ b/packages/npm-package-json-lint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.34.0-prerelease", + "version": "4.33.0", "description": "WordPress npm-package-json-lint shareable configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index a18c4016db340f..9980b2d629d0ba 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 8.11.0 (2023-12-27) - ## 8.10.0 (2023-12-13) ## 8.9.0 (2023-11-29) diff --git a/packages/nux/package.json b/packages/nux/package.json index 9af2b3682d3d1a..64511b2fa42d02 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "8.11.0-prerelease", + "version": "8.10.0", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/patterns/CHANGELOG.md b/packages/patterns/CHANGELOG.md index 707ab9ad1be6aa..566bd024cf3805 100644 --- a/packages/patterns/CHANGELOG.md +++ b/packages/patterns/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 1.10.0 (2023-12-27) - ## 1.9.0 (2023-12-13) ## 1.8.0 (2023-11-29) diff --git a/packages/patterns/package.json b/packages/patterns/package.json index 7334017ca1a435..54ec5178d640d6 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/patterns", - "version": "1.10.0-prerelease", + "version": "1.9.0", "description": "Management of user pattern editing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index b12e4c9d21bedf..137c14222ced34 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -28,11 +28,25 @@ import CategorySelector, { CATEGORY_SLUG } from './category-selector'; import { unlock } from '../lock-unlock'; export default function CreatePatternModal( { + className = 'patterns-menu-items__convert-modal', + modalTitle = __( 'Create pattern' ), + ...restProps +} ) { + return ( + <Modal + title={ modalTitle } + onRequestClose={ restProps.onClose } + overlayClassName={ className } + > + <CreatePatternModalContents { ...restProps } /> + </Modal> + ); +} + +export function CreatePatternModalContents( { confirmLabel = __( 'Create' ), defaultCategories = [], - className = 'patterns-menu-items__convert-modal', content, - modalTitle = __( 'Create pattern' ), onClose, onError, onSuccess, @@ -63,24 +77,27 @@ export default function CreatePatternModal( { const categoryMap = useMemo( () => { // Merge the user and core pattern categories and remove any duplicates. const uniqueCategories = new Map(); - [ ...userPatternCategories, ...corePatternCategories ].forEach( - ( category ) => { - if ( - ! uniqueCategories.has( category.label ) && - // There are two core categories with `Post` label so explicitly remove the one with - // the `query` slug to avoid any confusion. - category.name !== 'query' - ) { - // We need to store the name separately as this is used as the slug in the - // taxonomy and may vary from the label. - uniqueCategories.set( category.label, { - label: category.label, - value: category.label, - name: category.name, - } ); - } + userPatternCategories.forEach( ( category ) => { + uniqueCategories.set( category.label.toLowerCase(), { + label: category.label, + name: category.name, + id: category.id, + } ); + } ); + + corePatternCategories.forEach( ( category ) => { + if ( + ! uniqueCategories.has( category.label.toLowerCase() ) && + // There are two core categories with `Post` label so explicitly remove the one with + // the `query` slug to avoid any confusion. + category.name !== 'query' + ) { + uniqueCategories.set( category.label.toLowerCase(), { + label: category.label, + name: category.name, + } ); } - ); + } ); return uniqueCategories; }, [ userPatternCategories, corePatternCategories ] ); @@ -126,9 +143,13 @@ export default function CreatePatternModal( { */ async function findOrCreateTerm( term ) { try { - // We need to match any existing term to the correct slug to prevent duplicates, eg. - // the core `Headers` category uses the singular `header` as the slug. - const existingTerm = categoryMap.get( term ); + const existingTerm = categoryMap.get( term.toLowerCase() ); + if ( existingTerm && existingTerm.id ) { + return existingTerm.id; + } + // If we have an existing core category we need to match the new user category to the + // correct slug rather than autogenerating it to prevent duplicates, eg. the core `Headers` + // category uses the singular `header` as the slug. const termData = existingTerm ? { name: existingTerm.label, slug: existingTerm.name } : { name: term }; @@ -148,78 +169,68 @@ export default function CreatePatternModal( { return error.data.term_id; } } - return ( - <Modal - title={ modalTitle } - onRequestClose={ () => { - onClose(); - setTitle( '' ); + <form + onSubmit={ ( event ) => { + event.preventDefault(); + onCreate( title, syncType ); } } - overlayClassName={ className } > - <form - onSubmit={ ( event ) => { - event.preventDefault(); - onCreate( title, syncType ); - } } - > - <VStack spacing="5"> - <TextControl - label={ __( 'Name' ) } - value={ title } - onChange={ setTitle } - placeholder={ __( 'My pattern' ) } - className="patterns-create-modal__name-input" - __nextHasNoMarginBottom + <VStack spacing="5"> + <TextControl + label={ __( 'Name' ) } + value={ title } + onChange={ setTitle } + placeholder={ __( 'My pattern' ) } + className="patterns-create-modal__name-input" + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + <CategorySelector + categoryTerms={ categoryTerms } + onChange={ setCategoryTerms } + categoryMap={ categoryMap } + /> + <ToggleControl + label={ _x( + 'Synced', + 'Option that makes an individual pattern synchronized' + ) } + help={ __( + 'Sync this pattern across multiple locations.' + ) } + checked={ syncType === PATTERN_SYNC_TYPES.full } + onChange={ () => { + setSyncType( + syncType === PATTERN_SYNC_TYPES.full + ? PATTERN_SYNC_TYPES.unsynced + : PATTERN_SYNC_TYPES.full + ); + } } + /> + <HStack justify="right"> + <Button __next40pxDefaultSize - /> - <CategorySelector - categoryTerms={ categoryTerms } - onChange={ setCategoryTerms } - categoryMap={ categoryMap } - /> - <ToggleControl - label={ _x( - 'Synced', - 'Option that makes an individual pattern synchronized' - ) } - help={ __( - 'Sync this pattern across multiple locations.' - ) } - checked={ syncType === PATTERN_SYNC_TYPES.full } - onChange={ () => { - setSyncType( - syncType === PATTERN_SYNC_TYPES.full - ? PATTERN_SYNC_TYPES.unsynced - : PATTERN_SYNC_TYPES.full - ); + variant="tertiary" + onClick={ () => { + onClose(); + setTitle( '' ); } } - /> - <HStack justify="right"> - <Button - __next40pxDefaultSize - variant="tertiary" - onClick={ () => { - onClose(); - setTitle( '' ); - } } - > - { __( 'Cancel' ) } - </Button> + > + { __( 'Cancel' ) } + </Button> - <Button - __next40pxDefaultSize - variant="primary" - type="submit" - aria-disabled={ ! title || isSaving } - isBusy={ isSaving } - > - { confirmLabel } - </Button> - </HStack> - </VStack> - </form> - </Modal> + <Button + __next40pxDefaultSize + variant="primary" + type="submit" + aria-disabled={ ! title || isSaving } + isBusy={ isSaving } + > + { confirmLabel } + </Button> + </HStack> + </VStack> + </form> ); } diff --git a/packages/patterns/src/components/duplicate-pattern-modal.js b/packages/patterns/src/components/duplicate-pattern-modal.js index a62e7306dc90e1..6fa25a16f8594c 100644 --- a/packages/patterns/src/components/duplicate-pattern-modal.js +++ b/packages/patterns/src/components/duplicate-pattern-modal.js @@ -29,11 +29,7 @@ function getTermLabels( pattern, categories ) { .map( ( category ) => category.label ); } -export default function DuplicatePatternModal( { - pattern, - onClose, - onSuccess, -} ) { +export function useDuplicatePatternProps( { pattern, onSuccess } ) { const { createSuccessNotice } = useDispatch( noticesStore ); const categories = useSelect( ( select ) => { const { getUserPatternCategories, getBlockPatternCategories } = @@ -44,12 +40,10 @@ export default function DuplicatePatternModal( { user: getUserPatternCategories(), }; } ); - if ( ! pattern ) { return null; } - - const duplicatedProps = { + return { content: pattern.content, defaultCategories: getTermLabels( pattern, categories ), defaultSyncType: @@ -63,31 +57,39 @@ export default function DuplicatePatternModal( { ? pattern.title : pattern.title.raw ), - }; + onSuccess: ( { pattern: newPattern } ) => { + createSuccessNotice( + sprintf( + // translators: %s: The new pattern's title e.g. 'Call to action (copy)'. + __( '"%s" duplicated.' ), + newPattern.title.raw + ), + { + type: 'snackbar', + id: 'patterns-create', + } + ); - function handleOnSuccess( { pattern: newPattern } ) { - createSuccessNotice( - sprintf( - // translators: %s: The new pattern's title e.g. 'Call to action (copy)'. - __( '"%s" duplicated.' ), - newPattern.title.raw - ), - { - type: 'snackbar', - id: 'patterns-create', - } - ); + onSuccess?.( { pattern: newPattern } ); + }, + }; +} - onSuccess?.( { pattern: newPattern } ); +export default function DuplicatePatternModal( { + pattern, + onClose, + onSuccess, +} ) { + const duplicatedProps = useDuplicatePatternProps( { pattern, onSuccess } ); + if ( ! pattern ) { + return null; } - return ( <CreatePatternModal modalTitle={ __( 'Duplicate pattern' ) } confirmLabel={ __( 'Duplicate' ) } onClose={ onClose } onError={ onClose } - onSuccess={ handleOnSuccess } { ...duplicatedProps } /> ); diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index b357efb1bc107a..046e20dd300039 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -2,8 +2,14 @@ * Internal dependencies */ import { lock } from './lock-unlock'; -import CreatePatternModal from './components/create-pattern-modal'; -import DuplicatePatternModal from './components/duplicate-pattern-modal'; +import { + default as CreatePatternModal, + CreatePatternModalContents, +} from './components/create-pattern-modal'; +import { + default as DuplicatePatternModal, + useDuplicatePatternProps, +} from './components/duplicate-pattern-modal'; import RenamePatternModal from './components/rename-pattern-modal'; import PatternsMenuItems from './components'; import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; @@ -20,7 +26,9 @@ import { export const privateApis = {}; lock( privateApis, { CreatePatternModal, + CreatePatternModalContents, DuplicatePatternModal, + useDuplicatePatternProps, RenamePatternModal, PatternsMenuItems, RenamePatternCategoryModal, diff --git a/packages/plugins/CHANGELOG.md b/packages/plugins/CHANGELOG.md index 3a8419047a007f..b41c2e9f112bda 100644 --- a/packages/plugins/CHANGELOG.md +++ b/packages/plugins/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 6.17.0 (2023-12-27) - ## 6.16.0 (2023-12-13) ## 6.15.0 (2023-11-29) diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 11def4a122ebf9..b8b63a3c381d70 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/plugins", - "version": "6.17.0-prerelease", + "version": "6.16.0", "description": "Plugins module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/postcss-plugins-preset/CHANGELOG.md b/packages/postcss-plugins-preset/CHANGELOG.md index 03adf9363771a5..00e7fb9a8c6c4a 100644 --- a/packages/postcss-plugins-preset/CHANGELOG.md +++ b/packages/postcss-plugins-preset/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.33.0 (2023-12-27) - ## 4.32.0 (2023-12-13) ## 4.31.0 (2023-11-29) diff --git a/packages/postcss-plugins-preset/package.json b/packages/postcss-plugins-preset/package.json index 449c6b8994c4a0..078c6b87a2ec16 100644 --- a/packages/postcss-plugins-preset/package.json +++ b/packages/postcss-plugins-preset/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/postcss-plugins-preset", - "version": "4.33.0-prerelease", + "version": "4.32.0", "description": "PostCSS sharable plugins preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/postcss-themes/CHANGELOG.md b/packages/postcss-themes/CHANGELOG.md index b55e14922c0b1a..7c696adb50733f 100644 --- a/packages/postcss-themes/CHANGELOG.md +++ b/packages/postcss-themes/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 5.32.0 (2023-12-27) - ## 5.31.0 (2023-12-13) ## 5.30.0 (2023-11-29) diff --git a/packages/postcss-themes/package.json b/packages/postcss-themes/package.json index 9bad42995cd33e..845911ad918e5d 100644 --- a/packages/postcss-themes/package.json +++ b/packages/postcss-themes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/postcss-themes", - "version": "5.32.0-prerelease", + "version": "5.31.0", "description": "PostCSS plugin to generate theme colors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences-persistence/CHANGELOG.md b/packages/preferences-persistence/CHANGELOG.md index e06927f3a9328b..b227aa5ad23739 100644 --- a/packages/preferences-persistence/CHANGELOG.md +++ b/packages/preferences-persistence/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 1.41.0 (2023-12-27) - ## 1.40.0 (2023-12-13) ## 1.39.0 (2023-11-29) diff --git a/packages/preferences-persistence/package.json b/packages/preferences-persistence/package.json index 2a4fb8c576805e..e17080383749e8 100644 --- a/packages/preferences-persistence/package.json +++ b/packages/preferences-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences-persistence", - "version": "1.41.0-prerelease", + "version": "1.40.0", "description": "Persistence utilities for `wordpress/preferences`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js b/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js new file mode 100644 index 00000000000000..84542937563acd --- /dev/null +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/convert-editor-settings.js @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ + +export default function convertEditorSettings( data ) { + let newData = data; + const settingsToMoveToCore = [ + 'allowRightClickOverrides', + 'distractionFree', + 'fixedToolbar', + 'focusMode', + 'inactivePanels', + 'keepCaretInsideBlock', + 'mostUsedBlocks', + 'openPanels', + 'showBlockBreadcrumbs', + 'showIconLabels', + 'showListViewByDefault', + ]; + + settingsToMoveToCore.forEach( ( setting ) => { + if ( data?.[ 'core/edit-post' ]?.[ setting ] !== undefined ) { + newData = { + ...newData, + core: { + ...newData?.core, + [ setting ]: data[ 'core/edit-post' ][ setting ], + }, + }; + delete newData[ 'core/edit-post' ][ setting ]; + } + + if ( data?.[ 'core/edit-site' ]?.[ setting ] !== undefined ) { + delete newData[ 'core/edit-site' ][ setting ]; + } + } ); + + if ( Object.keys( newData?.[ 'core/edit-post' ] ?? {} )?.length === 0 ) { + delete newData[ 'core/edit-post' ]; + } + + if ( Object.keys( newData?.[ 'core/edit-site' ] ?? {} )?.length === 0 ) { + delete newData[ 'core/edit-site' ]; + } + + return newData; +} diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/index.js b/packages/preferences-persistence/src/migrations/preferences-package-data/index.js index 91efe2dac88f76..d4966e00e73d27 100644 --- a/packages/preferences-persistence/src/migrations/preferences-package-data/index.js +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/index.js @@ -2,7 +2,10 @@ * Internal dependencies */ import convertComplementaryAreas from './convert-complementary-areas'; +import convertEditorSettings from './convert-editor-settings'; export default function convertPreferencesPackageData( data ) { - return convertComplementaryAreas( data ); + let newData = convertComplementaryAreas( data ); + newData = convertEditorSettings( newData ); + return newData; } diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/test/convert-editor-settings.js b/packages/preferences-persistence/src/migrations/preferences-package-data/test/convert-editor-settings.js new file mode 100644 index 00000000000000..97a36f9630ecac --- /dev/null +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/test/convert-editor-settings.js @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import convertEditorSettings from '../convert-editor-settings'; + +describe( 'convertEditorSettings', () => { + it( 'converts the `allowRightClickOverrides` property', () => { + const input = { + 'core/edit-post': { + allowRightClickOverrides: false, + }, + }; + + const expectedOutput = { + core: { + allowRightClickOverrides: false, + }, + }; + + expect( convertEditorSettings( input ) ).toEqual( expectedOutput ); + } ); +} ); diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/test/index.js b/packages/preferences-persistence/src/migrations/preferences-package-data/test/index.js index 77888461ea078f..ffa39e630f5099 100644 --- a/packages/preferences-persistence/src/migrations/preferences-package-data/test/index.js +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/test/index.js @@ -42,22 +42,24 @@ describe( 'convertPreferencesPackageData', () => { expect( convertPreferencesPackageData( input ) ) .toMatchInlineSnapshot( ` { + "core": { + "fixedToolbar": true, + "inactivePanels": [], + "openPanels": [ + "post-status", + ], + }, "core/customize-widgets": { "fixedToolbar": true, "welcomeGuide": false, }, "core/edit-post": { "editorMode": "visual", - "fixedToolbar": true, "fullscreenMode": false, "hiddenBlockTypes": [ "core/audio", "core/cover", ], - "inactivePanels": [], - "openPanels": [ - "post-status", - ], "pinnedItems": { "my-sidebar-plugin/title-sidebar": false, }, @@ -67,7 +69,6 @@ describe( 'convertPreferencesPackageData', () => { "welcomeGuide": false, }, "core/edit-site": { - "fixedToolbar": true, "isComplementaryAreaVisible": true, "welcomeGuide": false, "welcomeGuideStyles": false, diff --git a/packages/preferences/CHANGELOG.md b/packages/preferences/CHANGELOG.md index 127686f9afc67a..363f62c70b3d93 100644 --- a/packages/preferences/CHANGELOG.md +++ b/packages/preferences/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.26.0 (2023-12-27) - ## 3.25.0 (2023-12-13) ## 3.24.0 (2023-11-29) diff --git a/packages/preferences/package.json b/packages/preferences/package.json index c67447b2aa3719..dc44878577aaf4 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences", - "version": "3.26.0-prerelease", + "version": "3.25.0", "description": "Utilities for managing WordPress preferences.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/prettier-config/CHANGELOG.md b/packages/prettier-config/CHANGELOG.md index 2fdf78bd0a0072..be847570c06f78 100644 --- a/packages/prettier-config/CHANGELOG.md +++ b/packages/prettier-config/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.6.0 (2023-12-27) - ## 3.5.0 (2023-12-13) ## 3.4.0 (2023-11-29) diff --git a/packages/prettier-config/package.json b/packages/prettier-config/package.json index c2e69e453f691e..047963ddde99b9 100644 --- a/packages/prettier-config/package.json +++ b/packages/prettier-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/prettier-config", - "version": "3.6.0-prerelease", + "version": "3.5.0", "description": "WordPress Prettier shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/primitives/CHANGELOG.md b/packages/primitives/CHANGELOG.md index 4e8e55c8d2fd87..bbea3951de7c95 100644 --- a/packages/primitives/CHANGELOG.md +++ b/packages/primitives/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.47.0 (2023-12-27) - ## 3.46.0 (2023-12-13) ## 3.45.0 (2023-11-29) diff --git a/packages/primitives/package.json b/packages/primitives/package.json index 294ba82270d362..614e586c7efe27 100644 --- a/packages/primitives/package.json +++ b/packages/primitives/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/primitives", - "version": "3.47.0-prerelease", + "version": "3.46.0", "description": "WordPress cross-platform primitives.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/priority-queue/CHANGELOG.md b/packages/priority-queue/CHANGELOG.md index 07df7fe79a6a50..dc82da748d2eb0 100644 --- a/packages/priority-queue/CHANGELOG.md +++ b/packages/priority-queue/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 2.49.0 (2023-12-27) - ## 2.48.0 (2023-12-13) ## 2.47.0 (2023-11-29) diff --git a/packages/priority-queue/package.json b/packages/priority-queue/package.json index a90dc43ed04768..4bfbb09da8d57c 100644 --- a/packages/priority-queue/package.json +++ b/packages/priority-queue/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/priority-queue", - "version": "2.49.0-prerelease", + "version": "2.48.0", "description": "Generic browser priority queue.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/private-apis/CHANGELOG.md b/packages/private-apis/CHANGELOG.md index 9d848ed4a7a140..4951a8b29a7dc1 100644 --- a/packages/private-apis/CHANGELOG.md +++ b/packages/private-apis/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 0.31.0 (2023-12-27) - ## 0.30.0 (2023-12-13) ## 0.29.0 (2023-11-29) diff --git a/packages/private-apis/package.json b/packages/private-apis/package.json index 953430fc97d1c2..654c53a02210e9 100644 --- a/packages/private-apis/package.json +++ b/packages/private-apis/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/private-apis", - "version": "0.31.0-prerelease", + "version": "0.30.0", "description": "Internal experimental APIs for WordPress core.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/private-apis/src/implementation.js b/packages/private-apis/src/implementation.js index a7da5bc9726554..619478cf76386d 100644 --- a/packages/private-apis/src/implementation.js +++ b/packages/private-apis/src/implementation.js @@ -24,6 +24,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/edit-site', '@wordpress/edit-widgets', '@wordpress/editor', + '@wordpress/format-library', '@wordpress/patterns', '@wordpress/reusable-blocks', '@wordpress/router', diff --git a/packages/project-management-automation/CHANGELOG.md b/packages/project-management-automation/CHANGELOG.md index 2ad1cfc39d4b77..09247de37995de 100644 --- a/packages/project-management-automation/CHANGELOG.md +++ b/packages/project-management-automation/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 1.48.0 (2023-12-27) - ## 1.47.0 (2023-12-13) ## 1.46.0 (2023-11-29) diff --git a/packages/project-management-automation/package.json b/packages/project-management-automation/package.json index 02ddae86044ebd..5e3d1f85314e23 100644 --- a/packages/project-management-automation/package.json +++ b/packages/project-management-automation/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/project-management-automation", - "version": "1.48.0-prerelease", + "version": "1.47.0", "description": "GitHub Action that implements various automation to assist with managing the Gutenberg GitHub repository.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/react-i18n/CHANGELOG.md b/packages/react-i18n/CHANGELOG.md index 7e7cba115149f4..4e15abf8fadfb5 100644 --- a/packages/react-i18n/CHANGELOG.md +++ b/packages/react-i18n/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.47.0 (2023-12-27) - ## 3.46.0 (2023-12-13) ## 3.45.0 (2023-11-29) diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json index fa81c2bba5deca..753f5138a2a0f9 100644 --- a/packages/react-i18n/package.json +++ b/packages/react-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-i18n", - "version": "3.47.0-prerelease", + "version": "3.46.0", "description": "React bindings for @wordpress/i18n.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java index 71df8e0c2888a2..380cdd1c5d6132 100644 --- a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java @@ -1,5 +1,7 @@ package org.wordpress.mobile.ReactNativeAztec; +import static android.content.ClipData.Item; + import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -10,18 +12,19 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - import android.text.Editable; import android.text.InputType; import android.text.Spannable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.View; +import android.view.ViewTreeObserver; import android.view.inputmethod.InputMethodManager; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.ThemedReactContext; @@ -41,12 +44,10 @@ import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.Set; -import java.util.HashSet; -import java.util.HashMap; - -import static android.content.ClipData.*; public class ReactAztecText extends AztecText { @@ -64,6 +65,7 @@ public class ReactAztecText extends AztecText { private @Nullable TextWatcherDelegator mTextWatcherDelegator; private @Nullable ContentSizeWatcher mContentSizeWatcher; private @Nullable ScrollWatcher mScrollWatcher; + private @Nullable Runnable mKeyboardRunnable; // FIXME: Used in `incrementAndGetEventCounter` but never read. I guess we can get rid of it, but before this // check when it's used in EditText in RN. (maybe tests?) @@ -264,18 +266,46 @@ public boolean requestFocus(int direction, Rect previouslyFocusedRect) { } private void showSoftKeyboard() { - new Handler(Looper.getMainLooper()).post(new Runnable() { + // If the text input is already focused we can show the keyboard. + if(hasWindowFocus()) { + showSoftKeyboardNow(); + } + // Otherwise, we'll wait until it gets focused. + else { + getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() { + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (hasFocus) { + showSoftKeyboardNow(); + getViewTreeObserver().removeOnWindowFocusChangeListener(this); + } + } + }); + } + } + + private void showSoftKeyboardNow() { + // Cancel any previously scheduled Runnable + if (mKeyboardRunnable != null) { + removeCallbacks(mKeyboardRunnable); + } + + mKeyboardRunnable = new Runnable() { @Override public void run() { if (mInputMethodManager != null) { - mInputMethodManager.showSoftInput(ReactAztecText.this, 0); + mInputMethodManager.showSoftInput(ReactAztecText.this, InputMethodManager.SHOW_IMPLICIT); } } - }); + }; + + post(mKeyboardRunnable); } private void hideSoftKeyboard() { - mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + if (mInputMethodManager != null) { + mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); + } } public void setScrollWatcher(ScrollWatcher scrollWatcher) { diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index 4b24bdbc707562..f91b214758b49a 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.109.3", + "version": "1.110.0", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index ca752d36e3d048..f76f8ed4f24501 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -142,7 +142,7 @@ export const focus = ( element ) => { // will take precedence and cancels pending blur events. blur.cancel(); // Similar to blur events, we also need to cancel potential keyboard dismiss. - dismissKeyboardDebounce.cancel(); + blurOnUnmountDebounce.cancel(); TextInputState.focusTextInput( element ); notifyInputChange(); @@ -164,13 +164,6 @@ export const blur = debounce( ( element ) => { /** * Unfocuses the specified element in case it's about to be unmounted. * - * On iOS text inputs are automatically unfocused and keyboard dismissed when they - * are removed. However, this is not the case on Android, where text inputs are - * unfocused but the keyboard remains open. - * - * For dismissing the keyboard, we use debounce to avoid conflicts with the focus - * event when both are triggered at the same time. - * * Note that we can't trigger the blur event, as it's likely that the Aztec view is no * longer available when the event is executed and will produce an exception. * @@ -181,13 +174,25 @@ export const blurOnUnmount = ( element ) => { // If a blur event was triggered before unmount, we need to cancel them to avoid // exceptions. blur.cancel(); - if ( Platform.OS === 'android' ) { - dismissKeyboardDebounce(); - } + blurOnUnmountDebounce(); } }; -const dismissKeyboardDebounce = debounce( () => hideAndroidSoftKeyboard(), 0 ); +// For updating the input state and dismissing the keyboard, we use debounce to avoid +// conflicts with the focus event when both are triggered at the same time. +const blurOnUnmountDebounce = debounce( () => { + // At this point, the text input will be destroyed but it's still focused. Hence, we + // have to explicitly notify listeners and update internal input state. + notifyListeners( { isFocused: false } ); + currentFocusedElement = null; + + // On iOS text inputs are automatically unfocused and keyboard dismissed when they + // are removed. However, this is not the case on Android, where text inputs are + // unfocused but the keyboard remains open. + if ( Platform.OS === 'android' ) { + hideAndroidSoftKeyboard(); + } +}, 0 ); /** * Unfocuses the current focused element. diff --git a/packages/react-native-aztec/src/test/AztecInputState.test.js b/packages/react-native-aztec/src/test/AztecInputState.test.js index e95d25a695c964..161f4b22bdfcd5 100644 --- a/packages/react-native-aztec/src/test/AztecInputState.test.js +++ b/packages/react-native-aztec/src/test/AztecInputState.test.js @@ -12,44 +12,45 @@ import { isFocused, focus, blur, - notifyInputChange, + blurOnUnmount, removeFocusChangeListener, } from '../AztecInputState'; -jest.mock( - 'react-native/Libraries/Components/TextInput/TextInputState', - () => ( { - focusTextInput: jest.fn(), - blurTextInput: jest.fn(), - currentlyFocusedInput: jest.fn(), - } ) -); +// Recreate internal state of TextInput +jest.mock( 'react-native/Libraries/Components/TextInput/TextInputState', () => { + let currentInput = null; + return { + focusTextInput: jest.fn( ( value ) => { + currentInput = value; + } ), + blurTextInput: jest.fn( ( value ) => { + if ( currentInput === value ) { + currentInput = null; + } + } ), + currentlyFocusedInput: jest.fn( () => currentInput ), + }; +} ); const ref = { current: null }; - -const updateCurrentFocusedInput = ( value ) => { - TextInputState.currentlyFocusedInput.mockReturnValue( value ); - notifyInputChange(); -}; +const anotherRef = { current: null }; jest.useFakeTimers(); describe( 'Aztec Input State', () => { it( 'listens to focus change event', () => { const listener = jest.fn(); - const anotherRef = { current: null }; addFocusChangeListener( listener ); - updateCurrentFocusedInput( ref ); - + focus( ref ); expect( listener ).toHaveBeenCalledWith( { isFocused: true } ); - updateCurrentFocusedInput( anotherRef ); - - expect( listener ).toHaveBeenCalledTimes( 1 ); - - updateCurrentFocusedInput( null ); + listener.mockClear(); + focus( anotherRef ); + expect( listener ).not.toHaveBeenCalled(); + blur( anotherRef ); + jest.runAllTimers(); expect( listener ).toHaveBeenCalledWith( { isFocused: false } ); } ); @@ -58,36 +59,35 @@ describe( 'Aztec Input State', () => { addFocusChangeListener( listener ); removeFocusChangeListener( listener ); - updateCurrentFocusedInput( ref ); - - expect( listener ).not.toHaveBeenCalledWith( { isFocused: true } ); - - updateCurrentFocusedInput( null ); + focus( ref ); + expect( listener ).not.toHaveBeenCalled(); - expect( listener ).not.toHaveBeenCalledWith( { isFocused: false } ); + blur( ref ); + jest.runAllTimers(); + expect( listener ).not.toHaveBeenCalled(); } ); - it( 'returns true if an element is focused', () => { - updateCurrentFocusedInput( ref ); + it( 'returns the focus state', () => { + focus( ref ); expect( isFocused() ).toBeTruthy(); - } ); - it( 'returns false if an element is unfocused', () => { - updateCurrentFocusedInput( null ); + blur( ref ); + jest.runAllTimers(); expect( isFocused() ).toBeFalsy(); } ); it( 'returns current focused element', () => { - const anotherRef = { current: null }; - updateCurrentFocusedInput( ref ); + focus( ref ); expect( getCurrentFocusedElement() ).toBe( ref ); - updateCurrentFocusedInput( anotherRef ); + focus( anotherRef ); expect( getCurrentFocusedElement() ).toBe( anotherRef ); } ); it( 'returns null if focused element is unfocused', () => { - updateCurrentFocusedInput( null ); + focus( ref ); + blur( ref ); + jest.runAllTimers(); expect( getCurrentFocusedElement() ).toBe( null ); } ); @@ -101,4 +101,49 @@ describe( 'Aztec Input State', () => { jest.runAllTimers(); expect( TextInputState.blurTextInput ).toHaveBeenCalledWith( ref ); } ); + + it( 'unfocuses an element when unmounted', () => { + const listener = jest.fn(); + + focus( ref ); + + addFocusChangeListener( listener ); + blurOnUnmount( ref ); + jest.runAllTimers(); + + // TextInputState will update its state internally when the text + // input is removed. For this reason and to avoid triggering an + // event on an removed element, we don't call blurTextInput. + expect( TextInputState.blurTextInput ).not.toHaveBeenCalled(); + expect( listener ).toHaveBeenCalledWith( { isFocused: false } ); + expect( listener ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'cancels blur event when unfocusing an element that will be unmounted', () => { + const listener = jest.fn(); + + focus( ref ); + + addFocusChangeListener( listener ); + blur( ref ); + blurOnUnmount( ref ); + jest.runAllTimers(); + + // TextInputState will update its state internally when the text + // input is removed. For this reason and to avoid triggering an + // event on an removed element, we don't call blurTextInput. + expect( TextInputState.blurTextInput ).not.toHaveBeenCalled(); + expect( listener ).toHaveBeenCalledWith( { isFocused: false } ); + expect( listener ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'cancels blur event when focusing an element', () => { + focus( ref ); + blur( ref ); + focus( anotherRef ); + jest.runAllTimers(); + + expect( TextInputState.focusTextInput ).toHaveBeenCalledTimes( 2 ); + expect( TextInputState.blurTextInput ).not.toHaveBeenCalled(); + } ); } ); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index c1dc4bab896b3a..62848ad68f12e4 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -32,6 +32,7 @@ interface MediaUploadEventEmitter { void onMediaFileUploadProgress(int mediaId, float progress); void onMediaFileUploadSucceeded(int mediaId, String mediaUrl, int serverId, WritableNativeMap metadata); void onMediaFileUploadFailed(int mediaId); + void onMediaFileUploadPaused(int mediaId); } interface MediaSaveEventEmitter { diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java index fe83bc8a14b540..3bbc8fe7429532 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java @@ -31,6 +31,7 @@ public interface JSEventEmitter { private static final int MEDIA_UPLOAD_STATE_SUCCEEDED = 2; private static final int MEDIA_UPLOAD_STATE_FAILED = 3; private static final int MEDIA_UPLOAD_STATE_RESET = 4; + private static final int MEDIA_UPLOAD_STATE_PAUSED = 11; private static final int MEDIA_SAVE_STATE_SAVING = 5; private static final int MEDIA_SAVE_STATE_SUCCEEDED = 6; @@ -180,6 +181,11 @@ public void onMediaFileUploadFailed(int mediaId) { setMediaFileUploadDataInJS(MEDIA_UPLOAD_STATE_FAILED, mediaId, null, 0); } + @Override + public void onMediaFileUploadPaused(int mediaId) { + setMediaFileUploadDataInJS(MEDIA_UPLOAD_STATE_PAUSED, mediaId, null, 0); + } + // Media file save events emitter @Override public void onSaveMediaFileClear(String mediaId) { diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt index ce427be2ad09b0..ec847d71bf51c9 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt @@ -92,7 +92,6 @@ data class GutenbergProps @JvmOverloads constructor( content?.let { putString(PROP_INITIAL_DATA, it) } } - private const val PROP_INITIAL_TITLE = "initialTitle" private const val PROP_INITIAL_HTML_MODE_ENABLED = "initialHtmlModeEnabled" private const val PROP_POST_TYPE = "postType" private const val PROP_HOST_APP_NAMESPACE = "hostAppNamespace" @@ -105,6 +104,7 @@ data class GutenbergProps @JvmOverloads constructor( private const val PROP_QUOTE_BLOCK_V2 = "quoteBlockV2" private const val PROP_LIST_BLOCK_V2 = "listBlockV2" + const val PROP_INITIAL_TITLE = "initialTitle" const val PROP_INITIAL_DATA = "initialData" const val PROP_STYLES = "rawStyles" const val PROP_FEATURES = "rawFeatures" diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index c0916d1417a34f..6098f15a6927b1 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -113,6 +113,7 @@ public class WPAndroidGlueCode { private OnToggleRedoButtonListener mOnToggleRedoButtonListener; private OnConnectionStatusEventListener mOnConnectionStatusEventListener; + private OnBackHandlerEventListener mOnBackHandlerEventListener; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -134,6 +135,7 @@ public class WPAndroidGlueCode { private boolean mIsDarkMode; private Consumer<Exception> mExceptionLogger; private Consumer<String> mBreadcrumbLogger; + private boolean mShouldHandleBackPress = false; public void onCreate(Context context) { SoLoader.init(context, /* native exopackage */ false); @@ -147,6 +149,10 @@ public boolean hasReactContext() { return mReactContext != null; } + public boolean shouldHandleBackPress() { + return mShouldHandleBackPress; + } + public boolean isContentChanged() { return mContentChanged; } @@ -264,6 +270,10 @@ public interface OnConnectionStatusEventListener { boolean onRequestConnectionStatus(); } + public interface OnBackHandlerEventListener { + void onBackHandler(); + } + public void mediaSelectionCancelled() { mAppendsMultipleSelectedToSiblingBlocks = false; } @@ -700,6 +710,7 @@ public void attachToContainer(ViewGroup viewGroup, OnToggleUndoButtonListener onToggleUndoButtonListener, OnToggleRedoButtonListener onToggleRedoButtonListener, OnConnectionStatusEventListener onConnectionStatusEventListener, + OnBackHandlerEventListener onBackHandlerEventListener, boolean isDarkMode) { MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext(); contextWrapper.setBaseContext(viewGroup.getContext()); @@ -726,6 +737,7 @@ public void attachToContainer(ViewGroup viewGroup, mOnToggleUndoButtonListener = onToggleUndoButtonListener; mOnToggleRedoButtonListener = onToggleRedoButtonListener; mOnConnectionStatusEventListener = onConnectionStatusEventListener; + mOnBackHandlerEventListener = onBackHandlerEventListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); @@ -761,6 +773,7 @@ private void refocus() { } public void onPause(Activity activity) { + mShouldHandleBackPress = false; if (mReactInstanceManager != null) { // get the focused view so we re-focus it later if needed. WeakReference so we don't leak it. mLastFocusedView = new WeakReference<>(mReactRootView.findFocus()); @@ -770,13 +783,14 @@ public void onPause(Activity activity) { } public void onResume(final Fragment fragment, final Activity activity) { + mShouldHandleBackPress = true; if (mReactInstanceManager != null) { mReactInstanceManager.onHostResume(activity, new DefaultHardwareBackBtnHandler() { @Override public void invokeDefaultOnBackPressed() { if (fragment.isAdded()) { - activity.onBackPressed(); + mOnBackHandlerEventListener.onBackHandler(); } } }); @@ -784,11 +798,13 @@ public void invokeDefaultOnBackPressed() { } public void onDetach(Activity activity) { + mShouldHandleBackPress = false; mReactInstanceManager.onHostDestroy(activity); mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().notifyModalClosed(); } public void onDestroy(Activity activity) { + mShouldHandleBackPress = false; if (mReactRootView != null) { mReactRootView.unmountReactApplication(); mReactRootView = null; @@ -804,6 +820,12 @@ public void onDestroy(Activity activity) { } } + public void onBackPressed() { + if (mReactInstanceManager != null) { + mReactInstanceManager.onBackPressed(); + } + } + public void showDevOptionsDialog() { mReactInstanceManager.showDevOptionsDialog(); } @@ -1125,6 +1147,10 @@ public void mediaFileUploadFailed(final int mediaId) { mDeferredEventEmitter.onMediaFileUploadFailed(mediaId); } + public void mediaFileUploadPaused(final int mediaId) { + mDeferredEventEmitter.onMediaFileUploadPaused(mediaId); + } + public void mediaFileUploadSucceeded(final int mediaId, final String mediaUrl, final int serverMediaId, final WritableNativeMap metadata) { mDeferredEventEmitter.onMediaFileUploadSucceeded(mediaId, mediaUrl, serverMediaId, metadata); diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index ffdd07a1640f78..de5eb78516ead9 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -3,6 +3,11 @@ */ import { NativeModules, NativeEventEmitter, Platform } from 'react-native'; +/** + * WordPress dependencies + */ +import RCTAztecView from '@wordpress/react-native-aztec'; + const { RNReactNativeGutenbergBridge } = NativeModules; const isIOS = Platform.OS === 'ios'; const isAndroid = Platform.OS === 'android'; @@ -489,7 +494,11 @@ export function showAndroidSoftKeyboard() { return; } - RNReactNativeGutenbergBridge.showAndroidSoftKeyboard(); + const hasFocusedTextInput = RCTAztecView.InputState.isFocused(); + + if ( hasFocusedTextInput ) { + RNReactNativeGutenbergBridge.showAndroidSoftKeyboard(); + } } /** diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index de0d1b513f00dc..1a3c0479646796 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -296,6 +296,7 @@ extension Gutenberg { case succeeded = 2 case failed = 3 case reset = 4 + case paused = 11 } public enum MediaSaveState: Int, MediaState { diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index 586c5159ef165f..276e536dbf929f 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.109.3", + "version": "1.110.0", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index fc12b7df655cd5..381ce0e93e4454 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,9 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [**] Image block media uploads display a custom error message when there is no internet connection [#56937] + +## 1.110.0 - [*] [internal] Move InserterButton from components package to block-editor package [#56494] - [*] [internal] Move ImageLinkDestinationsScreen from components package to block-editor package [#56775] - [*] Fix crash when blockType wrapperProps are not defined [#56846] @@ -17,6 +20,8 @@ For each user feature we should also add a importance categorization label to i - [**] Fix crash when sharing unsupported media types on Android [#56791] - [**] Fix regressions with wrapper props and font size customization [#56985] - [***] Avoid keyboard dismiss when interacting with text blocks [#57070] +- [**] Auto-scroll upon block insertion [#57273] +- [*] Unselect blocks using the hardware back button (Android) [#57279] ## 1.109.3 - [**] Fix duplicate/unresponsive options in font size settings. [#56985] diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js index e7ee4a20df03f2..230c844491d282 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-device-actions.test.js @@ -27,6 +27,10 @@ describe( 'Gutenberg Editor Rotation tests', () => { await editorPage.addNewBlock( blockNames.paragraph ); if ( isAndroid() ) { + // Waits until the keyboard is visible + await editorPage.driver.waitUntil( + editorPage.driver.isKeyboardShown + ); await editorPage.dismissKeyboard(); } diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js deleted file mode 100644 index 50a2a3ee8fd640..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; -import testData from './helpers/test-data'; - -describe( 'Gutenberg Editor tests', () => { - it( 'should be able to create a post with heading and paragraph blocks', async () => { - await editorPage.initializeEditor(); - await editorPage.addNewBlock( blockNames.heading ); - let headingBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.heading - ); - - await editorPage.typeTextToTextBlock( - headingBlockElement, - testData.heading - ); - - await editorPage.addNewBlock( blockNames.paragraph ); - let paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.mediumText - ); - - await editorPage.addNewBlock( blockNames.paragraph ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 3 - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.mediumText - ); - - await editorPage.addNewBlock( blockNames.heading ); - headingBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.heading, - 4 - ); - await editorPage.typeTextToTextBlock( - headingBlockElement, - testData.heading - ); - - await editorPage.addNewBlock( blockNames.paragraph ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 5 - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.mediumText - ); - - // Assert that even though there are 5 blocks, there should only be 3 paragraph blocks - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js deleted file mode 100644 index 8f21ef04858fb6..00000000000000 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Internal dependencies - */ -import { blockNames } from './pages/editor-page'; -import { - backspace, - clickMiddleOfElement, - clickBeginningOfElement, -} from './helpers/utils'; -import testData from './helpers/test-data'; - -describe( 'Gutenberg Editor tests for Paragraph Block', () => { - it( 'should be able to split one paragraph block into two', async () => { - await editorPage.initializeEditor(); - await editorPage.addNewBlock( blockNames.paragraph ); - const paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.shortText - ); - await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - '\n', - false - ); - const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); - expect( text0 ).not.toBe( '' ); - expect( text1 ).not.toBe( '' ); - expect( testData.shortText ).toMatch( - new RegExp( `${ text0 + text1 }|${ text0 } ${ text1 }` ) - ); - } ); - - it( 'should be able to merge 2 paragraph blocks into 1', async () => { - await editorPage.initializeEditor(); - await editorPage.addNewBlock( blockNames.paragraph ); - let paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph - ); - - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - testData.shortText - ); - await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); - - const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 - ); - - await clickBeginningOfElement( - editorPage.driver, - paragraphBlockElement - ); - - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - backspace - ); - - const text = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - expect( text0 + text1 ).toMatch( text ); - paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 1 - ); - await paragraphBlockElement.click(); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 1 ); - } ); - - it( 'should be able to create a post with multiple paragraph blocks', async () => { - await editorPage.initializeEditor(); - await editorPage.addNewBlock( blockNames.paragraph ); - await editorPage.sendTextToParagraphBlock( 1, testData.longText ); - expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 ); - } ); - - it( 'should be able to merge blocks with unknown html elements', async () => { - await editorPage.initializeEditor( { - initialData: [ - testData.unknownElementParagraphBlock, - testData.lettersInParagraphBlock, - ].join( '\n\n' ), - } ); - - // Merge paragraphs. - const paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 - ); - - const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); - - await clickBeginningOfElement( - editorPage.driver, - paragraphBlockElement - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - backspace - ); - - // Verify the editor has not crashed. - const mergedBlockText = - await editorPage.getTextForParagraphBlockAtPosition( 1 ); - expect( text0 + text1 ).toMatch( mergedBlockText ); - } ); - - // Based on https://github.com/wordpress-mobile/gutenberg-mobile/pull/1507 - it( 'should handle multiline paragraphs from web', async () => { - await editorPage.initializeEditor( { - initialData: [ - testData.multiLinesParagraphBlock, - testData.paragraphBlockEmpty, - ].join( '\n\n' ), - } ); - - // Merge paragraphs. - const paragraphBlockElement = await editorPage.getTextBlockAtPosition( - blockNames.paragraph, - 2 - ); - await editorPage.typeTextToTextBlock( - paragraphBlockElement, - backspace - ); - - // Verify the editor has not crashed. - const text = await editorPage.getTextForParagraphBlockAtPosition( 1 ); - expect( text.length ).not.toEqual( 0 ); - } ); -} ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js new file mode 100644 index 00000000000000..3a12bf5d13345b --- /dev/null +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-writing-flow-@canary.test.js @@ -0,0 +1,333 @@ +/** + * Internal dependencies + */ +import { blockNames } from './pages/editor-page'; +import { + backspace, + clickBeginningOfElement, + waitForMediaLibrary, +} from './helpers/utils'; +import testData from './helpers/test-data'; + +describe( 'Gutenberg Editor Writing flow tests', () => { + it( 'should be able to write a post title', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + const titleInput = await editorPage.getEmptyTitleTextInputElement(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + await editorPage.typeTextToTextBlock( titleInput, testData.shortText ); + + // Trigger the return key to go to the first Paragraph + await editorPage.typeTextToTextBlock( titleInput, '\n' ); + + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + expect( paragraphBlockElement ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + // Trigger the return key to delete the Paragraph block + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + backspace + ); + // Expect to have an empty Paragraph block and the keyboard visible + expect( + await editorPage.getTextBlockAtPosition( blockNames.paragraph ) + ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + } ); + + it( 'should be able to create a new Paragraph block when pressing the enter key', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.shortText + ); + await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); + + // Expect to have a new Paragraph block and the keyboard visible + expect( + await editorPage.getTextBlockAtPosition( blockNames.paragraph, 2 ) + ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + } ); + + it( 'should automatically dismiss the keyboard when selecting non-text-based-blocks', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + await editorPage.addNewBlock( blockNames.image ); + // Wait for the Media picker to show up + await waitForMediaLibrary( editorPage.driver ); + + // Select the WordPress Media Library option + await editorPage.chooseMediaLibrary(); + + // Wait until the media is added + await editorPage.driver.pause( 500 ); + + const captionElement = await editorPage.getImageBlockCaptionButton(); + await captionElement.click(); + const captionInput = + await editorPage.getImageBlockCaptionInput( captionElement ); + + expect( captionInput ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + // Sets a new caption + await editorPage.typeTextToTextBlock( + captionInput, + testData.listItem2, + true + ); + + // Trigger the return key to exit the caption and create a new Paragraph block + await editorPage.typeTextToTextBlock( captionInput, '\n' ); + + // Expect to have an empty Paragraph block and the keyboard visible + let paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + expect( paragraphBlockElement ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + backspace + ); + + // When deleting the Paragraph block, the keyboard should be hidden and + // the image block should be focused. + await editorPage.driver.waitUntil( async function () { + return ! ( await editorPage.driver.isKeyboardShown() ); + } ); + expect( await editorPage.isImageBlockSelected() ).toBe( true ); + + // Adding a new Paragraph block + await editorPage.addNewBlock( blockNames.paragraph ); + paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + + // It should be focused and the keyboard should be visible + expect( paragraphBlockElement ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.shortText + ); + + const imageBlockElement = await editorPage.getBlockAtPosition( + blockNames.image + ); + await imageBlockElement.click(); + + await editorPage.driver.waitUntil( async function () { + return ! ( await editorPage.driver.isKeyboardShown() ); + } ); + expect( await editorPage.isImageBlockSelected() ).toBe( true ); + } ); + + it( 'should manually dismiss the keyboard', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + expect( paragraphBlockElement ).toBeTruthy(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + await editorPage.dismissKeyboard(); + + // Checks that no block is selected by looking for the block menu actions button + expect( await editorPage.isBlockActionsMenuButtonDisplayed() ).toBe( + false + ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( false ); + } ); + + it( 'should dismiss the keyboard and show it back when opening modals', async () => { + await editorPage.initializeEditor( { initialTitle: '' } ); + + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + await editorPage.openBlockSettings(); + await editorPage.driver.waitUntil( async function () { + return ! ( await editorPage.driver.isKeyboardShown() ); + } ); + + await editorPage.dismissBottomSheet(); + + await editorPage.driver.waitUntil( editorPage.driver.isKeyboardShown ); + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.listItem1 + ); + const typedText = await paragraphBlockElement.getText(); + expect( typedText ).toMatch( testData.listItem1 ); + } ); + + it( 'should be able to split and merge paragraph blocks', async () => { + await editorPage.initializeEditor(); + + // Add the first Paragraph block using the default block appender + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + // Type text into the first Paragraph block + const firstParagraphBlockElement = + await editorPage.getTextBlockAtPosition( blockNames.paragraph ); + await editorPage.typeTextToTextBlock( + firstParagraphBlockElement, + testData.shortText + ); + + // Add a second Paragraph block and type some text + await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock(); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + const secondParagraphBlockElement = + await editorPage.getTextBlockAtPosition( blockNames.paragraph, 2 ); + await editorPage.typeTextToTextBlock( + secondParagraphBlockElement, + testData.mediumText + ); + + // Merge Paragraph blocks + await clickBeginningOfElement( + editorPage.driver, + secondParagraphBlockElement + ); + await editorPage.typeTextToTextBlock( + secondParagraphBlockElement, + backspace + ); + + // Wait for blocks to be merged + await editorPage.driver.waitUntil( async function () { + return ( await editorPage.getNumberOfParagraphBlocks() ) === 1; + } ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + // Split the current Paragraph block right where the caret is positioned + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 1, + true + ); + await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); + + // Wait for blocks to be split + await editorPage.driver.waitUntil( async function () { + return ( await editorPage.getNumberOfParagraphBlocks() ) === 2; + } ); + expect( await editorPage.driver.isKeyboardShown() ).toBe( true ); + + const firstParagraphText = + await editorPage.getTextForParagraphBlockAtPosition( 1 ); + const secondParagraphText = + await editorPage.getTextForParagraphBlockAtPosition( 2 ); + + expect( firstParagraphText ).toEqual( testData.shortText ); + expect( secondParagraphText ).toEqual( testData.mediumText ); + } ); + + it( 'should be able to create a post with multiple paragraph blocks', async () => { + await editorPage.initializeEditor(); + const defaultBlockAppenderElement = + await editorPage.getDefaultBlockAppenderElement(); + await defaultBlockAppenderElement.click(); + + await editorPage.sendTextToParagraphBlock( 1, testData.longText ); + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 ); + } ); + + it( 'should be able to merge blocks with unknown html elements', async () => { + await editorPage.initializeEditor( { + initialData: [ + testData.unknownElementParagraphBlock, + testData.lettersInParagraphBlock, + ].join( '\n\n' ), + } ); + + // Merge paragraphs. + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + + const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); + const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); + + await clickBeginningOfElement( + editorPage.driver, + paragraphBlockElement + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + backspace + ); + + // Verify the editor has not crashed. + const mergedBlockText = + await editorPage.getTextForParagraphBlockAtPosition( 1 ); + expect( text0 + text1 ).toMatch( mergedBlockText ); + } ); + + it( 'should be able to create a post with heading and paragraph blocks', async () => { + await editorPage.initializeEditor(); + await editorPage.addNewBlock( blockNames.heading ); + const headingBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.heading + ); + + await editorPage.typeTextToTextBlock( + headingBlockElement, + testData.heading + ); + + await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock(); + let paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 2 + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.mediumText + ); + + await editorPage.addParagraphBlockByTappingEmptyAreaBelowLastBlock(); + paragraphBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.paragraph, + 3 + ); + await editorPage.typeTextToTextBlock( + paragraphBlockElement, + testData.mediumText + ); + + // Assert that even though there are 3 blocks, there should only be 2 paragraph blocks + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 2 ); + } ); +} ); diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index a19aaf5445d79f..b00be20458e802 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -45,8 +45,18 @@ class EditorPage { } } - async initializeEditor( { initialData, rawStyles, rawFeatures } = {} ) { - await launchApp( this.driver, { initialData, rawStyles, rawFeatures } ); + async initializeEditor( { + initialTitle, + initialData, + rawStyles, + rawFeatures, + } = {} ) { + await launchApp( this.driver, { + initialTitle, + initialData, + rawStyles, + rawFeatures, + } ); // Stores initial values from the editor for different helpers. const addButton = await this.driver.$$( `~${ ADD_BLOCK_ID }` ); @@ -72,9 +82,13 @@ class EditorPage { // Text blocks functions // E.g. Paragraph, Heading blocks // =============================== - async getTextBlockAtPosition( blockName, position = 1 ) { + async getTextBlockAtPosition( + blockName, + position = 1, + skipWrapperClick = false + ) { // iOS needs a click to get the text element - if ( ! isAndroid() ) { + if ( ! isAndroid() && ! skipWrapperClick ) { const textBlockLocator = `(//XCUIElementTypeButton[contains(@name, "${ blockName } Block. Row ${ position }")])`; await clickIfClickable( this.driver, textBlockLocator ); @@ -171,14 +185,25 @@ class EditorPage { } async addParagraphBlockByTappingEmptyAreaBelowLastBlock() { - const emptyAreaBelowLastBlock = - await this.driver.elementByAccessibilityId( 'Add paragraph block' ); + const element = isAndroid() + ? '~Add paragraph block' + : '(//XCUIElementTypeOther[@name="Add paragraph block"])'; + const emptyAreaBelowLastBlock = await this.driver.$( element ); await emptyAreaBelowLastBlock.click(); } - async getTitleElement( options = { autoscroll: false } ) { + async getDefaultBlockAppenderElement() { + const appenderElement = isAndroid() + ? `//android.widget.EditText[@text='Start writing…']` + : '(//XCUIElementTypeOther[contains(@name, "Start writing…")])[2]'; + return this.driver.$( appenderElement ); + } + + async getTitleElement( options = { autoscroll: false, isEmpty: false } ) { const titleElement = isAndroid() - ? 'Post title. Welcome to Gutenberg!' + ? `Post title. ${ + options.isEmpty ? 'Empty' : 'Welcome to Gutenberg!' + }` : 'post-title'; if ( options.autoscroll ) { @@ -200,6 +225,18 @@ class EditorPage { return elements[ 0 ]; } + async getEmptyTitleTextInputElement() { + const titleWrapperElement = await this.getTitleElement( { + isEmpty: true, + } ); + await titleWrapperElement.click(); + + const titleElement = isAndroid() + ? '//android.widget.EditText[@content-desc="Post title. Empty"]' + : '~Add title'; + return this.driver.$( titleElement ); + } + // iOS loads the block list more eagerly compared to Android. // This makes this function return elements without scrolling on iOS. // So we are keeping this Android only. @@ -370,10 +407,14 @@ class EditorPage { await settingsButton.click(); } - async removeBlock() { - const blockActionsButtonElement = isAndroid() + getBlockActionsMenuElement() { + return isAndroid() ? '//android.widget.Button[contains(@content-desc, "Open Block Actions Menu")]' : '//XCUIElementTypeButton[@name="Open Block Actions Menu"]'; + } + + async removeBlock() { + const blockActionsButtonElement = this.getBlockActionsMenuElement(); const blockActionsMenu = await this.swipeToolbarToElement( blockActionsButtonElement ); @@ -391,6 +432,12 @@ class EditorPage { return await swipeDown( this.driver ); } + async isBlockActionsMenuButtonDisplayed() { + const menuButtonElement = this.getBlockActionsMenuElement(); + const elementsFound = await this.driver.$$( menuButtonElement ); + return elementsFound.length !== 0; + } + // ========================= // Block toolbar functions // ========================= @@ -406,8 +453,6 @@ class EditorPage { swipeRight: true, } ); await addButton[ 0 ].click(); - // Wait for Bottom sheet animation to finish - await this.driver.pause( 3000 ); } // Click on block of choice. @@ -425,10 +470,9 @@ class EditorPage { const inserterElement = isAndroid() ? 'Blocks menu' : 'InserterUI-Blocks'; - return await this.waitForElementToBeDisplayedById( - inserterElement, - 4000 - ); + await this.driver + .$( `~${ inserterElement }` ) + .waitForDisplayed( { timeout: 4000 } ); } static async isElementOutOfBounds( element, { width, height } = {} ) { @@ -787,13 +831,25 @@ class EditorPage { await clickIfClickable( this.driver, mediaLibraryLocator ); } + async getImageBlockCaptionButton() { + const captionElement = isAndroid() + ? '//android.widget.Button[starts-with(@content-desc, "Image caption")]' + : '//XCUIElementTypeButton[starts-with(@name, "Image caption.")]'; + return this.driver.$( captionElement ); + } + + async getImageBlockCaptionInput( imageBlockCaptionButton ) { + const captionInputElement = isAndroid() + ? '//android.widget.EditText' + : '//XCUIElementTypeTextView'; + return imageBlockCaptionButton.$( captionInputElement ); + } + async enterCaptionToSelectedImageBlock( caption, clear = true ) { - const imageBlockCaptionButton = await this.driver.$( - '//XCUIElementTypeButton[starts-with(@name, "Image caption.")]' - ); + const imageBlockCaptionButton = await this.getImageBlockCaptionButton(); await imageBlockCaptionButton.click(); - const imageBlockCaptionField = await imageBlockCaptionButton.$( - '//XCUIElementTypeTextView' + const imageBlockCaptionField = await this.getImageBlockCaptionInput( + imageBlockCaptionButton ); await typeString( this.driver, imageBlockCaptionField, caption, clear ); } @@ -814,6 +870,16 @@ class EditorPage { .perform(); } + async isImageBlockSelected() { + // Since there isn't an easy way to see if a block is selected, + // it will check if the edit image button is visible + const editImageElement = isAndroid() + ? '(//android.widget.Button[@content-desc="Edit image"])' + : '(//XCUIElementTypeButton[@name="Edit image"])'; + + return await this.driver.$( editImageElement ).isDisplayed(); + } + // ============================= // Search Block functions // ============================= diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java index 69985317ddad33..3ea19fa97b3831 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java @@ -113,6 +113,8 @@ protected void onCreate(Bundle savedInstanceState) { LinearLayout linearLayout = new LinearLayout(this); linearLayout.setOrientation(LinearLayout.VERTICAL); linearLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + linearLayout.setFocusable(false); + linearLayout.setFocusableInTouchMode(true); // Create a Toolbar instance Toolbar toolbar = new Toolbar(this); @@ -166,6 +168,7 @@ private Bundle getAppOptions() { Bundle bundle = new Bundle(); // Parse initial props from launch arguments + String initialTitle = null; String initialData = null; String rawStyles = null; String rawFeatures = null; @@ -175,6 +178,9 @@ private Bundle getAppOptions() { String initialProps = extrasBundle.getString(EXTRAS_INITIAL_PROPS, "{}"); try { JSONObject jsonObject = new JSONObject(initialProps); + if (jsonObject.has(GutenbergProps.PROP_INITIAL_TITLE)) { + initialTitle = jsonObject.getString(GutenbergProps.PROP_INITIAL_TITLE); + } if (jsonObject.has(GutenbergProps.PROP_INITIAL_DATA)) { initialData = jsonObject.getString(GutenbergProps.PROP_INITIAL_DATA); } @@ -209,6 +215,9 @@ private Bundle getAppOptions() { capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_SMARTFRAME_EMBED_BLOCK, true); bundle.putBundle(GutenbergProps.PROP_CAPABILITIES, capabilities); + if(initialTitle != null) { + bundle.putString(GutenbergProps.PROP_INITIAL_TITLE, initialTitle); + } if(initialData != null) { bundle.putString(GutenbergProps.PROP_INITIAL_DATA, initialData); } diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index ef95c7e65862f6..0c04308125df71 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -399,7 +399,10 @@ extension GutenbergViewController: GutenbergBridgeDataSource { } func gutenbergInitialTitle() -> String? { - return nil + guard isUITesting(), let initialProps = getInitialPropsFromArgs() else { + return nil + } + return initialProps["initialTitle"] } func gutenbergHostAppNamespace() -> String { diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index f4eaa1c15bc1f3..cf40d621834bda 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.11) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.109.3): + - Gutenberg (1.110.0): - React-Core (= 0.71.11) - React-CoreModules (= 0.71.11) - React-RCTImage (= 0.71.11) @@ -429,7 +429,7 @@ PODS: - React-RCTImage - RNSVG (13.9.0): - React-Core - - RNTAztecView (1.109.3): + - RNTAztecView (1.110.0): - React-Core - WordPress-Aztec-iOS (= 1.19.9) - SDWebImage (5.11.1): @@ -617,7 +617,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: f07662560742d82a5b73cee116c70b0b49bcc220 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - Gutenberg: 74c7183474e117f4ffaae5eac944cf598a383095 + Gutenberg: 758124df95be2159a16909fcf00e289b9299fa39 hermes-engine: 34c863b446d0135b85a6536fa5fd89f48196f848 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c @@ -662,7 +662,7 @@ SPEC CHECKSUMS: RNReanimated: d4f363f4987ae0ade3e36ff81c94e68261bf4b8d RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 - RNTAztecView: fd32ea370f13d9edd7f43b65b6270ae499757d69 + RNTAztecView: 75ea6f071cbdd0f0afe83de7b93c0691a2bebd21 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 90f99b36a0b0a8..0a8ceed231ae4f 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.109.3", + "version": "1.110.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/readable-js-assets-webpack-plugin/CHANGELOG.md b/packages/readable-js-assets-webpack-plugin/CHANGELOG.md index 341e6475e18906..3a5dad2c518521 100644 --- a/packages/readable-js-assets-webpack-plugin/CHANGELOG.md +++ b/packages/readable-js-assets-webpack-plugin/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 2.32.0 (2023-12-27) - ## 2.31.0 (2023-12-13) ## 2.30.0 (2023-11-29) diff --git a/packages/readable-js-assets-webpack-plugin/package.json b/packages/readable-js-assets-webpack-plugin/package.json index 518f2cc0a80537..d27c530ca15107 100644 --- a/packages/readable-js-assets-webpack-plugin/package.json +++ b/packages/readable-js-assets-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "2.32.0-prerelease", + "version": "2.31.0", "description": "Generate a readable JS file for each JS asset.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/redux-routine/CHANGELOG.md b/packages/redux-routine/CHANGELOG.md index 385dff820d64cf..9559858c133e45 100644 --- a/packages/redux-routine/CHANGELOG.md +++ b/packages/redux-routine/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.49.0 (2023-12-27) - ## 4.48.0 (2023-12-13) ## 4.47.0 (2023-11-29) diff --git a/packages/redux-routine/package.json b/packages/redux-routine/package.json index 82a296a97a0d68..2bdfb5fe8bffdb 100644 --- a/packages/redux-routine/package.json +++ b/packages/redux-routine/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/redux-routine", - "version": "4.49.0-prerelease", + "version": "4.48.0", "description": "Redux middleware for generator coroutines.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/reusable-blocks/CHANGELOG.md b/packages/reusable-blocks/CHANGELOG.md index f55d9897f7c825..989f649f161f23 100644 --- a/packages/reusable-blocks/CHANGELOG.md +++ b/packages/reusable-blocks/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.26.0 (2023-12-27) - ## 4.25.0 (2023-12-13) ## 4.24.0 (2023-11-29) diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json index 07deea38569930..c6e6df921270ab 100644 --- a/packages/reusable-blocks/package.json +++ b/packages/reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/reusable-blocks", - "version": "4.26.0-prerelease", + "version": "4.25.0", "description": "Reusable blocks utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index 41da0d5d730fac..ebc6157a84c1b1 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 6.26.0 (2023-12-27) - ## 6.25.0 (2023-12-13) ## 6.24.0 (2023-11-29) diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index ac91805807c571..645beb47bfa2a6 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "6.26.0-prerelease", + "version": "6.25.0", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index a35fabbd4e2fad..7c0989a11dc4a6 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -178,6 +178,19 @@ export class RichTextData { } } +for ( const name of Object.getOwnPropertyNames( String.prototype ) ) { + if ( RichTextData.prototype.hasOwnProperty( name ) ) { + continue; + } + + Object.defineProperty( RichTextData.prototype, name, { + value( ...args ) { + // Should we convert back to RichTextData? + return this.toHTMLString()[ name ]( ...args ); + }, + } ); +} + /** * Create a RichText value from an `Element` tree (DOM), an HTML string or a * plain text string, with optionally a `Range` object to set the selection. If diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md index 053a6cf0bbcc1d..90f93aa1f97001 100644 --- a/packages/router/CHANGELOG.md +++ b/packages/router/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 0.18.0 (2023-12-27) - ## 0.17.0 (2023-12-13) ## 0.16.0 (2023-11-29) diff --git a/packages/router/package.json b/packages/router/package.json index 76ca9cba46588c..d9d640a9fe75c3 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/router", - "version": "0.18.0-prerelease", + "version": "0.17.0", "description": "Router API for WordPress pages.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 294a3570108c4b..a917cd119c17a1 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased -## 27.0.0 (2023-12-27) +### New Features + +- Add experimental support for `viewModule` field in block.json for `build` and `start` scripts ([#57461](https://github.com/WordPress/gutenberg/pull/57461)). ### Breaking Changes diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 42a9c563ea77f7..e4a2be8d9a3fae 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -108,6 +108,11 @@ This script automatically use the optimized config but sometimes you may want to - `--webpack-src-dir` – Allows customization of the source code directory. Default is `src`. - `--output-path` – Allows customization of the output directory. Default is `build`. +Experimental support for the block.json `viewModule` field is available via the +`--experimental-modules` option. With this option enabled, script and module fields will all be +compiled. The `viewModule` field is analogous to the `viewScript` field, but will compile a module +and should be registered in WordPress using the Modules API. + #### Advanced information This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. @@ -391,6 +396,11 @@ This script automatically use the optimized config but sometimes you may want to - `--webpack-src-dir` – Allows customization of the source code directory. Default is `src`. - `--output-path` – Allows customization of the output directory. Default is `build`. +Experimental support for the block.json `viewModule` field is available via the +`--experimental-modules` option. With this option enabled, script and module fields will all be +compiled. The `viewModule` field is analogous to the `viewScript` field, but will compile a module +and should be registered in WordPress using the Modules API. + #### Advanced information This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. @@ -723,8 +733,8 @@ module.exports = { If you follow this approach, please, be aware that: -- You should keep using the `wp-scripts` commands (`start` and `build`). Do not use `webpack` directly. -- Future versions of this package may change what webpack and Babel plugins we bundle, default configs, etc. Should those changes be necessary, they will be registered in the [package’s CHANGELOG](https://github.com/WordPress/gutenberg/blob/HEAD/packages/scripts/CHANGELOG.md), so make sure to read it before upgrading. +- You should keep using the `wp-scripts` commands (`start` and `build`). Do not use `webpack` directly. +- Future versions of this package may change what webpack and Babel plugins we bundle, default configs, etc. Should those changes be necessary, they will be registered in the [package’s CHANGELOG](https://github.com/WordPress/gutenberg/blob/HEAD/packages/scripts/CHANGELOG.md), so make sure to read it before upgrading. ## Contributing to this package diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js index 1e060d0e142c91..57bd258d325393 100644 --- a/packages/scripts/config/webpack.config.js +++ b/packages/scripts/config/webpack.config.js @@ -4,7 +4,7 @@ const { BundleAnalyzerPlugin } = require( 'webpack-bundle-analyzer' ); const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); -const { DefinePlugin } = require( 'webpack' ); +const webpack = require( 'webpack' ); const browserslist = require( 'browserslist' ); const MiniCSSExtractPlugin = require( 'mini-css-extract-plugin' ); const { basename, dirname, resolve } = require( 'path' ); @@ -30,6 +30,9 @@ const { getWordPressSrcDirectory, getWebpackEntryPoints, getRenderPropPaths, + getAsBooleanFromENV, + getBlockJsonModuleFields, + getBlockJsonScriptFields, } = require( '../utils' ); const isProduction = process.env.NODE_ENV === 'production'; @@ -39,6 +42,9 @@ if ( ! browserslist.findConfig( '.' ) ) { target += ':' + fromConfigRoot( '.browserslistrc' ); } const hasReactFastRefresh = hasArgInCLI( '--hot' ) && ! isProduction; +const hasExperimentalModulesFlag = getAsBooleanFromENV( + 'WP_EXPERIMENTAL_MODULES' +); /** * The plugin recomputes the render paths once on each compilation. It is necessary to avoid repeating processing @@ -110,10 +116,10 @@ const cssLoaders = [ }, ]; -const config = { +/** @type {webpack.Configuration} */ +const baseConfig = { mode, target, - entry: getWebpackEntryPoints, output: { filename: '[name].js', path: resolve( process.cwd(), 'build' ), @@ -165,7 +171,7 @@ const config = { module: { rules: [ { - test: /\.(j|t)sx?$/, + test: /\.m?(j|t)sx?$/, exclude: /node_modules/, use: [ { @@ -245,21 +251,72 @@ const config = { }, ], }, + stats: { + children: false, + }, +}; + +// WP_DEVTOOL global variable controls how source maps are generated. +// See: https://webpack.js.org/configuration/devtool/#devtool. +if ( process.env.WP_DEVTOOL ) { + baseConfig.devtool = process.env.WP_DEVTOOL; +} + +if ( ! isProduction ) { + // Set default sourcemap mode if it wasn't set by WP_DEVTOOL. + baseConfig.devtool = baseConfig.devtool || 'source-map'; +} + +// Add source-map-loader if devtool is set, whether in dev mode or not. +if ( baseConfig.devtool ) { + baseConfig.module.rules.unshift( { + test: /\.(j|t)sx?$/, + exclude: [ /node_modules/ ], + use: require.resolve( 'source-map-loader' ), + enforce: 'pre', + } ); +} + +/** @type {webpack.Configuration} */ +const scriptConfig = { + ...baseConfig, + + entry: getWebpackEntryPoints( 'script' ), + + devServer: isProduction + ? undefined + : { + devMiddleware: { + writeToDisk: true, + }, + allowedHosts: 'auto', + host: 'localhost', + port: 8887, + proxy: { + '/build': { + pathRewrite: { + '^/build': '', + }, + }, + }, + }, + plugins: [ - new DefinePlugin( { + new webpack.DefinePlugin( { // Inject the `SCRIPT_DEBUG` global, used for development features flagging. SCRIPT_DEBUG: ! isProduction, } ), - // During rebuilds, all webpack assets that are not used anymore will be - // removed automatically. There is an exception added in watch mode for - // fonts and images. It is a known limitations: - // https://github.com/johnagan/clean-webpack-plugin/issues/159 - new CleanWebpackPlugin( { - cleanAfterEveryBuildPatterns: [ '!fonts/**', '!images/**' ], - // Prevent it from deleting webpack assets during builds that have - // multiple configurations returned in the webpack config. - cleanStaleWebpackAssets: false, - } ), + + // If we run a modules build, the 2 compilations can "clean" each other's output + // Prevent the cleaning from happening + ! hasExperimentalModulesFlag && + new CleanWebpackPlugin( { + cleanAfterEveryBuildPatterns: [ '!fonts/**', '!images/**' ], + // Prevent it from deleting webpack assets during builds that have + // multiple configurations returned in the webpack config. + cleanStaleWebpackAssets: false, + } ), + new RenderPathsPlugin(), new CopyWebpackPlugin( { patterns: [ @@ -269,27 +326,33 @@ const config = { noErrorOnMissing: true, transform( content, absoluteFrom ) { const convertExtension = ( path ) => { - return path.replace( /\.(j|t)sx?$/, '.js' ); + return path.replace( /\.m?(j|t)sx?$/, '.js' ); }; if ( basename( absoluteFrom ) === 'block.json' ) { const blockJson = JSON.parse( content.toString() ); - [ 'viewScript', 'script', 'editorScript' ].forEach( - ( key ) => { - if ( Array.isArray( blockJson[ key ] ) ) { - blockJson[ key ] = - blockJson[ key ].map( - convertExtension - ); - } else if ( - typeof blockJson[ key ] === 'string' - ) { - blockJson[ key ] = convertExtension( - blockJson[ key ] - ); + + [ + getBlockJsonScriptFields( blockJson ), + getBlockJsonModuleFields( blockJson ), + ].forEach( ( fields ) => { + if ( fields ) { + for ( const [ + key, + value, + ] of Object.entries( fields ) ) { + if ( Array.isArray( value ) ) { + blockJson[ key ] = + value.map( convertExtension ); + } else if ( + typeof value === 'string' + ) { + blockJson[ key ] = + convertExtension( value ); + } } } - ); + } ); return JSON.stringify( blockJson, null, 2 ); } @@ -317,52 +380,59 @@ const config = { process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(), // MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript. new MiniCSSExtractPlugin( { filename: '[name].css' } ), - // React Fast Refresh. - hasReactFastRefresh && new ReactRefreshWebpackPlugin(), // WP_NO_EXTERNALS global variable controls whether scripts' assets get // generated, and the default externals set. ! process.env.WP_NO_EXTERNALS && new DependencyExtractionWebpackPlugin(), ].filter( Boolean ), - stats: { - children: false, - }, }; -// WP_DEVTOOL global variable controls how source maps are generated. -// See: https://webpack.js.org/configuration/devtool/#devtool. -if ( process.env.WP_DEVTOOL ) { - config.devtool = process.env.WP_DEVTOOL; -} +if ( hasExperimentalModulesFlag ) { + /** @type {webpack.Configuration} */ + const moduleConfig = { + ...baseConfig, -if ( ! isProduction ) { - // Set default sourcemap mode if it wasn't set by WP_DEVTOOL. - config.devtool = config.devtool || 'source-map'; - config.devServer = { - devMiddleware: { - writeToDisk: true, + entry: getWebpackEntryPoints( 'module' ), + + experiments: { + ...baseConfig.experiments, + outputModule: true, }, - allowedHosts: 'auto', - host: 'localhost', - port: 8887, - proxy: { - '/build': { - pathRewrite: { - '^/build': '', - }, + + output: { + ...baseConfig.output, + module: true, + chunkFormat: 'module', + environment: { + ...baseConfig.output.environment, + module: true, + }, + library: { + ...baseConfig.output.library, + type: 'module', }, }, + + plugins: [ + new webpack.DefinePlugin( { + // Inject the `SCRIPT_DEBUG` global, used for development features flagging. + SCRIPT_DEBUG: ! isProduction, + } ), + // The WP_BUNDLE_ANALYZER global variable enables a utility that represents + // bundle content as a convenient interactive zoomable treemap. + process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(), + // MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript. + new MiniCSSExtractPlugin( { filename: '[name].css' } ), + // React Fast Refresh. + hasReactFastRefresh && new ReactRefreshWebpackPlugin(), + // WP_NO_EXTERNALS global variable controls whether scripts' assets get + // generated, and the default externals set. + ! process.env.WP_NO_EXTERNALS && + new DependencyExtractionWebpackPlugin(), + ].filter( Boolean ), }; -} -// Add source-map-loader if devtool is set, whether in dev mode or not. -if ( config.devtool ) { - config.module.rules.unshift( { - test: /\.(j|t)sx?$/, - exclude: [ /node_modules/ ], - use: require.resolve( 'source-map-loader' ), - enforce: 'pre', - } ); + module.exports = [ scriptConfig, moduleConfig ]; +} else { + module.exports = scriptConfig; } - -module.exports = config; diff --git a/packages/scripts/package.json b/packages/scripts/package.json index dbca51c0dc7632..f97ab389a527b1 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "27.0.0-prerelease", + "version": "26.19.0", "description": "Collection of reusable scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/scripts/scripts/build.js b/packages/scripts/scripts/build.js index 714038fd80ee4e..0eef2afb451bfc 100644 --- a/packages/scripts/scripts/build.js +++ b/packages/scripts/scripts/build.js @@ -12,6 +12,10 @@ const EXIT_ERROR_CODE = 1; process.env.NODE_ENV = process.env.NODE_ENV || 'production'; +if ( hasArgInCLI( '--experimental-modules' ) ) { + process.env.WP_EXPERIMENTAL_MODULES = true; +} + if ( hasArgInCLI( '--webpack-no-externals' ) ) { process.env.WP_NO_EXTERNALS = true; } diff --git a/packages/scripts/scripts/start.js b/packages/scripts/scripts/start.js index cf29709f3eff15..6296192ef302b1 100644 --- a/packages/scripts/scripts/start.js +++ b/packages/scripts/scripts/start.js @@ -10,6 +10,10 @@ const { sync: resolveBin } = require( 'resolve-bin' ); const { getArgFromCLI, getWebpackArgs, hasArgInCLI } = require( '../utils' ); const EXIT_ERROR_CODE = 1; +if ( hasArgInCLI( '--experimental-modules' ) ) { + process.env.WP_EXPERIMENTAL_MODULES = true; +} + if ( hasArgInCLI( '--webpack-no-externals' ) ) { process.env.WP_NO_EXTERNALS = true; } diff --git a/packages/scripts/utils/block-json.js b/packages/scripts/utils/block-json.js new file mode 100644 index 00000000000000..892cc63c889e50 --- /dev/null +++ b/packages/scripts/utils/block-json.js @@ -0,0 +1,41 @@ +const moduleFields = new Set( [ 'viewModule' ] ); +const scriptFields = new Set( [ 'viewScript', 'script', 'editorScript' ] ); + +/** + * @param {Object} blockJson + * @return {null|Record<string, unknown>} Fields + */ +function getBlockJsonModuleFields( blockJson ) { + let result = null; + for ( const field of moduleFields ) { + if ( Object.hasOwn( blockJson, field ) ) { + if ( ! result ) { + result = {}; + } + result[ field ] = blockJson[ field ]; + } + } + return result; +} + +/** + * @param {Object} blockJson + * @return {null|Record<string, unknown>} Fields + */ +function getBlockJsonScriptFields( blockJson ) { + let result = null; + for ( const field of scriptFields ) { + if ( Object.hasOwn( blockJson, field ) ) { + if ( ! result ) { + result = {}; + } + result[ field ] = blockJson[ field ]; + } + } + return result; +} + +module.exports = { + getBlockJsonModuleFields, + getBlockJsonScriptFields, +}; diff --git a/packages/scripts/utils/config.js b/packages/scripts/utils/config.js index e4e42255f95dd3..8b1bbb1ca50590 100644 --- a/packages/scripts/utils/config.js +++ b/packages/scripts/utils/config.js @@ -17,6 +17,10 @@ const { } = require( './cli' ); const { fromConfigRoot, fromProjectRoot, hasProjectFile } = require( './file' ); const { hasPackageProp } = require( './package' ); +const { + getBlockJsonModuleFields, + getBlockJsonScriptFields, +} = require( './block-json' ); const { log } = console; // See https://babeljs.io/docs/en/config-files#configuration-file-types. @@ -108,7 +112,10 @@ const hasPostCSSConfig = () => */ const getWebpackArgs = () => { // Gets all args from CLI without those prefixed with `--webpack`. - let webpackArgs = getArgsFromCLI( [ '--webpack' ] ); + let webpackArgs = getArgsFromCLI( [ + '--experimental-modules', + '--webpack', + ] ); const hasWebpackOutputOption = hasArgInCLI( '-o' ) || hasArgInCLI( '--output' ); @@ -186,104 +193,52 @@ function getWordPressSrcDirectory() { * * @see https://webpack.js.org/concepts/entry-points/ * - * @return {Object<string,string>} The list of entry points. + * @param {'script' | 'module'} buildType */ -function getWebpackEntryPoints() { - // 1. Handles the legacy format for entry points when explicitly provided with the `process.env.WP_ENTRY`. - if ( process.env.WP_ENTRY ) { - return JSON.parse( process.env.WP_ENTRY ); - } +function getWebpackEntryPoints( buildType ) { + /** + * @return {Object<string,string>} The list of entry points. + */ + return () => { + // 1. Handles the legacy format for entry points when explicitly provided with the `process.env.WP_ENTRY`. + if ( process.env.WP_ENTRY ) { + return buildType === 'script' + ? JSON.parse( process.env.WP_ENTRY ) + : {}; + } - // Continue only if the source directory exists. - if ( ! hasProjectFile( getWordPressSrcDirectory() ) ) { - log( - chalk.yellow( - `Source directory "${ getWordPressSrcDirectory() }" was not found. Please confirm there is a "src" directory in the root or the value passed to --webpack-src-dir is correct.` - ) - ); - return {}; - } + // Continue only if the source directory exists. + if ( ! hasProjectFile( getWordPressSrcDirectory() ) ) { + log( + chalk.yellow( + `Source directory "${ getWordPressSrcDirectory() }" was not found. Please confirm there is a "src" directory in the root or the value passed to --webpack-src-dir is correct.` + ) + ); + return {}; + } - // 2. Checks whether any block metadata files can be detected in the defined source directory. - // It scans all discovered files looking for JavaScript assets and converts them to entry points. - const blockMetadataFiles = glob( '**/block.json', { - absolute: true, - cwd: fromProjectRoot( getWordPressSrcDirectory() ), - } ); + // 2. Checks whether any block metadata files can be detected in the defined source directory. + // It scans all discovered files looking for JavaScript assets and converts them to entry points. + const blockMetadataFiles = glob( '**/block.json', { + absolute: true, + cwd: fromProjectRoot( getWordPressSrcDirectory() ), + } ); + + if ( blockMetadataFiles.length > 0 ) { + const srcDirectory = fromProjectRoot( + getWordPressSrcDirectory() + sep + ); + + const entryPoints = {}; - if ( blockMetadataFiles.length > 0 ) { - const srcDirectory = fromProjectRoot( - getWordPressSrcDirectory() + sep - ); - const entryPoints = blockMetadataFiles.reduce( - ( accumulator, blockMetadataFile ) => { + for ( const blockMetadataFile of blockMetadataFiles ) { + const fileContents = readFileSync( blockMetadataFile ); + let parsedBlockJson; // wrapping in try/catch in case the file is malformed // this happens especially when new block.json files are added // at which point they are completely empty and therefore not valid JSON try { - const { editorScript, script, viewScript } = JSON.parse( - readFileSync( blockMetadataFile ) - ); - [ editorScript, script, viewScript ] - .flat() - .filter( - ( value ) => value && value.startsWith( 'file:' ) - ) - .forEach( ( value ) => { - // Removes the `file:` prefix. - const filepath = join( - dirname( blockMetadataFile ), - value.replace( 'file:', '' ) - ); - - // Takes the path without the file extension, and relative to the defined source directory. - if ( ! filepath.startsWith( srcDirectory ) ) { - log( - chalk.yellow( - `Skipping "${ value.replace( - 'file:', - '' - ) }" listed in "${ blockMetadataFile.replace( - fromProjectRoot( sep ), - '' - ) }". File is located outside of the "${ getWordPressSrcDirectory() }" directory.` - ) - ); - return; - } - const entryName = filepath - .replace( extname( filepath ), '' ) - .replace( srcDirectory, '' ) - .replace( /\\/g, '/' ); - - // Detects the proper file extension used in the defined source directory. - const [ entryFilepath ] = glob( - `${ entryName }.[jt]s?(x)`, - { - absolute: true, - cwd: fromProjectRoot( - getWordPressSrcDirectory() - ), - } - ); - - if ( ! entryFilepath ) { - log( - chalk.yellow( - `Skipping "${ value.replace( - 'file:', - '' - ) }" listed in "${ blockMetadataFile.replace( - fromProjectRoot( sep ), - '' - ) }". File does not exist in the "${ getWordPressSrcDirectory() }" directory.` - ) - ); - return; - } - accumulator[ entryName ] = entryFilepath; - } ); - return accumulator; + parsedBlockJson = JSON.parse( fileContents ); } catch ( error ) { chalk.yellow( `Skipping "${ blockMetadataFile.replace( @@ -291,35 +246,105 @@ function getWebpackEntryPoints() { '' ) }" due to malformed JSON.` ); - return accumulator; } - }, - {} - ); - if ( Object.keys( entryPoints ).length > 0 ) { - return entryPoints; + const fields = + buildType === 'script' + ? getBlockJsonScriptFields( parsedBlockJson ) + : getBlockJsonModuleFields( parsedBlockJson ); + + if ( ! fields ) { + continue; + } + + for ( const value of Object.values( fields ).flat() ) { + if ( ! value.startsWith( 'file:' ) ) { + continue; + } + + // Removes the `file:` prefix. + const filepath = join( + dirname( blockMetadataFile ), + value.replace( 'file:', '' ) + ); + + // Takes the path without the file extension, and relative to the defined source directory. + if ( ! filepath.startsWith( srcDirectory ) ) { + log( + chalk.yellow( + `Skipping "${ value.replace( + 'file:', + '' + ) }" listed in "${ blockMetadataFile.replace( + fromProjectRoot( sep ), + '' + ) }". File is located outside of the "${ getWordPressSrcDirectory() }" directory.` + ) + ); + return; + } + const entryName = filepath + .replace( extname( filepath ), '' ) + .replace( srcDirectory, '' ) + .replace( /\\/g, '/' ); + + // Detects the proper file extension used in the defined source directory. + const [ entryFilepath ] = glob( + `${ entryName }.?(m)[jt]s?(x)`, + { + absolute: true, + cwd: fromProjectRoot( getWordPressSrcDirectory() ), + } + ); + + if ( ! entryFilepath ) { + log( + chalk.yellow( + `Skipping "${ value.replace( + 'file:', + '' + ) }" listed in "${ blockMetadataFile.replace( + fromProjectRoot( sep ), + '' + ) }". File does not exist in the "${ getWordPressSrcDirectory() }" directory.` + ) + ); + return; + } + entryPoints[ entryName ] = entryFilepath; + } + } + + if ( Object.keys( entryPoints ).length > 0 ) { + return entryPoints; + } } - } - // 3. Checks whether a standard file name can be detected in the defined source directory, - // and converts the discovered file to entry point. - const [ entryFile ] = glob( 'index.[jt]s?(x)', { - absolute: true, - cwd: fromProjectRoot( getWordPressSrcDirectory() ), - } ); + // Don't do any further processing if this is a module build. + // This only respects *module block.json fields. + if ( buildType === 'module' ) { + return {}; + } - if ( ! entryFile ) { - log( - chalk.yellow( - `No entry file discovered in the "${ getWordPressSrcDirectory() }" directory.` - ) - ); - return {}; - } + // 3. Checks whether a standard file name can be detected in the defined source directory, + // and converts the discovered file to entry point. + const [ entryFile ] = glob( 'index.[jt]s?(x)', { + absolute: true, + cwd: fromProjectRoot( getWordPressSrcDirectory() ), + } ); + + if ( ! entryFile ) { + log( + chalk.yellow( + `No entry file discovered in the "${ getWordPressSrcDirectory() }" directory.` + ) + ); + return {}; + } - return { - index: entryFile, + return { + index: entryFile, + }; }; } diff --git a/packages/scripts/utils/index.js b/packages/scripts/utils/index.js index ae93160381df44..148895ecbc4edf 100644 --- a/packages/scripts/utils/index.js +++ b/packages/scripts/utils/index.js @@ -25,6 +25,10 @@ const { } = require( './config' ); const { fromProjectRoot, fromConfigRoot, hasProjectFile } = require( './file' ); const { getPackageProp, hasPackageProp } = require( './package' ); +const { + getBlockJsonModuleFields, + getBlockJsonScriptFields, +} = require( './block-json' ); module.exports = { fromProjectRoot, @@ -40,6 +44,8 @@ module.exports = { getWordPressSrcDirectory, getWebpackEntryPoints, getRenderPropPaths, + getBlockJsonModuleFields, + getBlockJsonScriptFields, hasArgInCLI, hasBabelConfig, hasCssnanoConfig, diff --git a/packages/server-side-render/CHANGELOG.md b/packages/server-side-render/CHANGELOG.md index 3334e398b0d1d2..89f9150ade561e 100644 --- a/packages/server-side-render/CHANGELOG.md +++ b/packages/server-side-render/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 4.26.0 (2023-12-27) - ## 4.25.0 (2023-12-13) ## 4.24.0 (2023-11-29) diff --git a/packages/server-side-render/package.json b/packages/server-side-render/package.json index 73aea8946a2551..c3e3bb66680e7a 100644 --- a/packages/server-side-render/package.json +++ b/packages/server-side-render/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/server-side-render", - "version": "4.26.0-prerelease", + "version": "4.25.0", "description": "The component used with WordPress to server-side render a preview of dynamic blocks to display in the editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/shortcode/CHANGELOG.md b/packages/shortcode/CHANGELOG.md index 0585984ecbec3f..6d4bb284be7d4a 100644 --- a/packages/shortcode/CHANGELOG.md +++ b/packages/shortcode/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.49.0 (2023-12-27) - ## 3.48.0 (2023-12-13) ## 3.47.0 (2023-11-29) diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index b640e88ac033fd..1e29a5e03343c2 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/shortcode", - "version": "3.49.0-prerelease", + "version": "3.48.0", "description": "Shortcode module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/style-engine/CHANGELOG.md b/packages/style-engine/CHANGELOG.md index e112c888904c8e..35331cb4fe9a4a 100644 --- a/packages/style-engine/CHANGELOG.md +++ b/packages/style-engine/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 1.32.0 (2023-12-27) - ## 1.31.0 (2023-12-13) ## 1.30.0 (2023-11-29) diff --git a/packages/style-engine/docs/using-the-style-engine-with-block-supports.md b/packages/style-engine/docs/using-the-style-engine-with-block-supports.md index 42350a17ffcd6d..27d80df189cf3d 100644 --- a/packages/style-engine/docs/using-the-style-engine-with-block-supports.md +++ b/packages/style-engine/docs/using-the-style-engine-with-block-supports.md @@ -231,4 +231,4 @@ array( */ ``` -Read more about [global styles](https://developer.wordpress.org/block-editor/explanations/architecture/styles/#global-styles) and [preset CSS custom properties](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-json/#css-custom-properties-presets-custom) and [theme supports](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/). +Read more about [global styles](https://developer.wordpress.org/block-editor/explanations/architecture/styles/#global-styles) and [preset CSS custom properties](https://developer.wordpress.org/block-editor/how-to-guides/themes/global-settings-and-styles.md#css-custom-properties-presets-custom) and [theme supports](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/). diff --git a/packages/style-engine/package.json b/packages/style-engine/package.json index a0ba7ef7d32c85..d11bc37c122bc2 100644 --- a/packages/style-engine/package.json +++ b/packages/style-engine/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/style-engine", - "version": "1.32.0-prerelease", + "version": "1.31.0", "description": "A suite of parsers and compilers for WordPress styles.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js index fac55b4000e58f..1727ed535897bc 100644 --- a/packages/style-engine/src/test/index.js +++ b/packages/style-engine/src/test/index.js @@ -224,6 +224,14 @@ describe( 'getCSSRules', () => { expect( getCSSRules( { + background: { + backgroundImage: { + source: 'file', + url: 'https://example.com/image.jpg', + }, + backgroundRepeat: 'no-repeat', + backgroundSize: '300px', + }, color: { text: '#dddddd', background: '#555555', @@ -371,6 +379,21 @@ describe( 'getCSSRules', () => { key: 'boxShadow', value: '10px 10px red', }, + { + selector: '.some-selector', + key: 'backgroundImage', + value: "url( 'https://example.com/image.jpg' )", + }, + { + selector: '.some-selector', + key: 'backgroundRepeat', + value: 'no-repeat', + }, + { + selector: '.some-selector', + key: 'backgroundSize', + value: '300px', + }, ] ); } ); @@ -400,4 +423,68 @@ describe( 'getCSSRules', () => { }, ] ); } ); + + it( 'should output fallback cover background size when no size is provided', () => { + expect( + getCSSRules( + { + background: { + backgroundImage: { + source: 'file', + url: 'https://example.com/image.jpg', + }, + }, + }, + { + selector: '.some-selector', + } + ) + ).toEqual( [ + { + selector: '.some-selector', + key: 'backgroundImage', + value: "url( 'https://example.com/image.jpg' )", + }, + { + selector: '.some-selector', + key: 'backgroundSize', + value: 'cover', + }, + ] ); + } ); + + it( 'should output fallback center position for contain background size', () => { + expect( + getCSSRules( + { + background: { + backgroundImage: { + source: 'file', + url: 'https://example.com/image.jpg', + }, + backgroundSize: 'contain', + }, + }, + { + selector: '.some-selector', + } + ) + ).toEqual( [ + { + selector: '.some-selector', + key: 'backgroundImage', + value: "url( 'https://example.com/image.jpg' )", + }, + { + selector: '.some-selector', + key: 'backgroundSize', + value: 'contain', + }, + { + selector: '.some-selector', + key: 'backgroundPosition', + value: 'center', + }, + ] ); + } ); } ); diff --git a/packages/stylelint-config/CHANGELOG.md b/packages/stylelint-config/CHANGELOG.md index d9e6551000a5e1..b7b6cb712ff759 100644 --- a/packages/stylelint-config/CHANGELOG.md +++ b/packages/stylelint-config/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 21.32.0 (2023-12-27) - ## 21.31.0 (2023-12-13) ## 21.30.0 (2023-11-29) diff --git a/packages/stylelint-config/package.json b/packages/stylelint-config/package.json index c07b9ff1eaad66..ff3ed08296c5e9 100644 --- a/packages/stylelint-config/package.json +++ b/packages/stylelint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/stylelint-config", - "version": "21.32.0-prerelease", + "version": "21.31.0", "description": "stylelint config for WordPress development.", "author": "The WordPress Contributors", "license": "MIT", diff --git a/packages/sync/CHANGELOG.md b/packages/sync/CHANGELOG.md index 67f84a206b6737..206d4957a12874 100644 --- a/packages/sync/CHANGELOG.md +++ b/packages/sync/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 0.11.0 (2023-12-27) - ## 0.10.0 (2023-12-13) ## 0.9.0 (2023-11-29) diff --git a/packages/sync/package.json b/packages/sync/package.json index 51c9ce8522a281..7f3e20f6bd75ad 100644 --- a/packages/sync/package.json +++ b/packages/sync/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/sync", - "version": "0.11.0-prerelease", + "version": "0.10.0", "description": "Sync Data.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/token-list/CHANGELOG.md b/packages/token-list/CHANGELOG.md index 496decbd686f45..7488e836b0c137 100644 --- a/packages/token-list/CHANGELOG.md +++ b/packages/token-list/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 2.49.0 (2023-12-27) - ## 2.48.0 (2023-12-13) ## 2.47.0 (2023-11-29) diff --git a/packages/token-list/package.json b/packages/token-list/package.json index b7807fe59eb7c2..3a877f9365d753 100644 --- a/packages/token-list/package.json +++ b/packages/token-list/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/token-list", - "version": "2.49.0-prerelease", + "version": "2.48.0", "description": "Constructable, plain JavaScript DOMTokenList implementation, supporting non-browser runtimes.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/undo-manager/CHANGELOG.md b/packages/undo-manager/CHANGELOG.md index da3817bdd9b8a1..2ff22f8020a32e 100644 --- a/packages/undo-manager/CHANGELOG.md +++ b/packages/undo-manager/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 0.9.0 (2023-12-27) - ## 0.8.0 (2023-12-13) ## 0.7.0 (2023-11-29) diff --git a/packages/undo-manager/package.json b/packages/undo-manager/package.json index e058bebc939c3f..f1d47b82de47d8 100644 --- a/packages/undo-manager/package.json +++ b/packages/undo-manager/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/undo-manager", - "version": "0.9.0-prerelease", + "version": "0.8.0", "description": "A small package to manage undo/redo.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index 22e781b1f02397..29ca8923380d96 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.50.0 (2023-12-27) - ## 3.49.0 (2023-12-13) ## 3.48.0 (2023-11-29) diff --git a/packages/url/package.json b/packages/url/package.json index e56260cf74b670..62cb1727928b4f 100644 --- a/packages/url/package.json +++ b/packages/url/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/url", - "version": "3.50.0-prerelease", + "version": "3.49.0", "description": "WordPress URL utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/CHANGELOG.md b/packages/viewport/CHANGELOG.md index 80cabc0b7fe3c1..4d03197988aeca 100644 --- a/packages/viewport/CHANGELOG.md +++ b/packages/viewport/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 5.26.0 (2023-12-27) - ## 5.25.0 (2023-12-13) ## 5.24.0 (2023-11-29) diff --git a/packages/viewport/package.json b/packages/viewport/package.json index 4bd1820ddffddf..9251dabe8752d8 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "5.26.0-prerelease", + "version": "5.25.0", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/warning/CHANGELOG.md b/packages/warning/CHANGELOG.md index 9ee7a2674bce4d..629291f88171be 100644 --- a/packages/warning/CHANGELOG.md +++ b/packages/warning/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 2.49.0 (2023-12-27) - ## 2.48.0 (2023-12-13) ## 2.47.0 (2023-11-29) diff --git a/packages/warning/package.json b/packages/warning/package.json index e89f8ea203b8f1..7a7e3958a922e5 100644 --- a/packages/warning/package.json +++ b/packages/warning/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/warning", - "version": "2.49.0-prerelease", + "version": "2.48.0", "description": "Warning utility for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/widgets/CHANGELOG.md b/packages/widgets/CHANGELOG.md index 54a66ddbff1aff..a24187330ba58a 100644 --- a/packages/widgets/CHANGELOG.md +++ b/packages/widgets/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.26.0 (2023-12-27) - ## 3.25.0 (2023-12-13) ## 3.24.0 (2023-11-29) diff --git a/packages/widgets/package.json b/packages/widgets/package.json index ad5a74db92e8d4..60d7c7c5c46968 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/widgets", - "version": "3.26.0-prerelease", + "version": "3.25.0", "description": "Functionality used by the widgets block editor in the Widgets screen and the Customizer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/wordcount/CHANGELOG.md b/packages/wordcount/CHANGELOG.md index 4903ccc56d4522..9fab4788c9f41c 100644 --- a/packages/wordcount/CHANGELOG.md +++ b/packages/wordcount/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 3.49.0 (2023-12-27) - ## 3.48.0 (2023-12-13) ## 3.47.0 (2023-11-29) diff --git a/packages/wordcount/package.json b/packages/wordcount/package.json index ac9400d8b1e3f8..5e76d6ed042c6a 100644 --- a/packages/wordcount/package.json +++ b/packages/wordcount/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/wordcount", - "version": "3.49.0-prerelease", + "version": "3.48.0", "description": "WordPress word count utility.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/patches/react-native+0.71.11.patch b/patches/react-native+0.71.11+001+initial.patch similarity index 100% rename from patches/react-native+0.71.11.patch rename to patches/react-native+0.71.11+001+initial.patch diff --git a/patches/react-native+0.71.11+002+boost-podspec.patch b/patches/react-native+0.71.11+002+boost-podspec.patch new file mode 100644 index 00000000000000..ad785b77a47fcc --- /dev/null +++ b/patches/react-native+0.71.11+002+boost-podspec.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-native/third-party-podspecs/boost.podspec b/node_modules/react-native/third-party-podspecs/boost.podspec +index 3d9331c..bbbb738 100644 +--- a/node_modules/react-native/third-party-podspecs/boost.podspec ++++ b/node_modules/react-native/third-party-podspecs/boost.podspec +@@ -10,7 +10,7 @@ Pod::Spec.new do |spec| + spec.homepage = 'http://www.boost.org' + spec.summary = 'Boost provides free peer-reviewed portable C++ source libraries.' + spec.authors = 'Rene Rivera' +- spec.source = { :http => 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', ++ spec.source = { :http => 'https://archives.boost.io/release/1.76.0/source/boost_1_76_0.tar.bz2', + :sha256 => 'f0397ba6e982c4450f27bf32a2a83292aba035b827a5623a14636ea583318c41' } + + # Pinning to the same version as React.podspec. diff --git a/phpunit/block-supports/background-test.php b/phpunit/block-supports/background-test.php index bdcfc02f551a7f..92e1d2fc345a0e 100644 --- a/phpunit/block-supports/background-test.php +++ b/phpunit/block-supports/background-test.php @@ -134,7 +134,24 @@ public function data_background_block_support() { 'source' => 'file', ), ), - 'expected_wrapper' => '<div style="background-image:url('https://example.com/image.jpg');background-size:cover;">Content</div>', + 'expected_wrapper' => '<div class="has-background" style="background-image:url('https://example.com/image.jpg');background-size:cover;">Content</div>', + 'wrapper' => '<div>Content</div>', + ), + 'background image style with contain, position, and repeat is applied' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-rules-are-output', + 'background_settings' => array( + 'backgroundImage' => true, + ), + 'background_style' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + 'source' => 'file', + ), + 'backgroundRepeat' => 'no-repeat', + 'backgroundSize' => 'contain', + ), + 'expected_wrapper' => '<div class="has-background" style="background-image:url('https://example.com/image.jpg');background-position:center;background-repeat:no-repeat;background-size:contain;">Content</div>', 'wrapper' => '<div>Content</div>', ), 'background image style is appended if a style attribute already exists' => array( @@ -149,8 +166,8 @@ public function data_background_block_support() { 'source' => 'file', ), ), - 'expected_wrapper' => '<div classname="wp-block-test" style="color: red;background-image:url('https://example.com/image.jpg');background-size:cover;">Content</div>', - 'wrapper' => '<div classname="wp-block-test" style="color: red">Content</div>', + 'expected_wrapper' => '<div class="wp-block-test has-background" style="color: red;background-image:url('https://example.com/image.jpg');background-size:cover;">Content</div>', + 'wrapper' => '<div class="wp-block-test" style="color: red">Content</div>', ), 'background image style is appended if a style attribute containing multiple styles already exists' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', @@ -164,8 +181,8 @@ public function data_background_block_support() { 'source' => 'file', ), ), - 'expected_wrapper' => '<div classname="wp-block-test" style="color: red;font-size: 15px;background-image:url('https://example.com/image.jpg');background-size:cover;">Content</div>', - 'wrapper' => '<div classname="wp-block-test" style="color: red;font-size: 15px;">Content</div>', + 'expected_wrapper' => '<div class="wp-block-test has-background" style="color: red;font-size: 15px;background-image:url('https://example.com/image.jpg');background-size:cover;">Content</div>', + 'wrapper' => '<div class="wp-block-test" style="color: red;font-size: 15px;">Content</div>', ), 'background image style is not applied if the block does not support background image' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', diff --git a/phpunit/experimental/interactivity-api/class-wp-interactivity-initial-state-test.php b/phpunit/experimental/interactivity-api/class-wp-interactivity-initial-state-test.php new file mode 100644 index 00000000000000..a95c3482ec80d1 --- /dev/null +++ b/phpunit/experimental/interactivity-api/class-wp-interactivity-initial-state-test.php @@ -0,0 +1,115 @@ +<?php +/** + * `WP_Interactivity_Initial_State` class test. + * + * @package Gutenberg + * @subpackage Interactivity API + */ + +/** + * Tests for the `WP_Interactivity_Initial_State` class. + * + * @group interactivity-api + * @covers WP_Interactivity_Initial_State + */ +class WP_Interactivity_Initial_State_Test extends WP_UnitTestCase { + public function set_up() { + parent::set_up(); + WP_Interactivity_Initial_State::reset(); + } + public function tear_down() { + WP_Interactivity_Initial_State::reset(); + parent::tear_down(); + } + + public function test_initial_state_should_be_empty() { + $this->assertEmpty( WP_Interactivity_Initial_State::get_data() ); + } + + public function test_initial_state_can_be_merged() { + $state = array( + 'a' => 1, + 'b' => 2, + 'nested' => array( + 'c' => 3, + ), + ); + WP_Interactivity_Initial_State::merge_state( 'core', $state ); + $this->assertSame( $state, WP_Interactivity_Initial_State::get_state( 'core' ) ); + } + + public function test_initial_state_can_be_extended() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'b' => 2 ) ); + WP_Interactivity_Initial_State::merge_state( 'custom', array( 'c' => 3 ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => 1, + 'b' => 2, + ), + 'custom' => array( + 'c' => 3, + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_existing_props_should_be_overwritten() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 'overwritten' ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => 'overwritten', + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_existing_indexed_arrays_should_be_replaced() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => array( 1, 2 ) ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => array( 3, 4 ) ) ); + $this->assertSame( + array( + 'core' => array( + 'a' => array( 3, 4 ), + ), + ), + WP_Interactivity_Initial_State::get_data() + ); + } + + public function test_initial_state_should_be_correctly_rendered() { + WP_Interactivity_Initial_State::merge_state( 'core', array( 'a' => 1 ) ); + WP_Interactivity_Initial_State::merge_state( 'core', array( 'b' => 2 ) ); + WP_Interactivity_Initial_State::merge_state( 'custom', array( 'c' => 3 ) ); + + ob_start(); + WP_Interactivity_Initial_State::render(); + $rendered = ob_get_clean(); + $this->assertSame( + '<script id="wp-interactivity-initial-state" type="application/json">{"core":{"a":1,"b":2},"custom":{"c":3}}</script>', + $rendered + ); + } + + public function test_initial_state_should_also_escape_tags_and_amps() { + WP_Interactivity_Initial_State::merge_state( + 'test', + array( + 'amps' => 'http://site.test/?foo=1&baz=2&bar=3', + 'tags' => 'Do not do this: <!-- <script>', + ) + ); + ob_start(); + WP_Interactivity_Initial_State::render(); + $rendered = ob_get_clean(); + $this->assertSame( + '<script id="wp-interactivity-initial-state" type="application/json">{"test":{"amps":"http:\/\/site.test\/?foo=1\u0026baz=2\u0026bar=3","tags":"Do not do this: \u003C!-- \u003Cscript\u003E"}}</script>', + $rendered + ); + } +} diff --git a/phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php b/phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php deleted file mode 100644 index 837d6fd50f193a..00000000000000 --- a/phpunit/experimental/interactivity-api/class-wp-interactivity-store-test.php +++ /dev/null @@ -1,186 +0,0 @@ -<?php -/** - * `WP_Interactivity_Store` class test. - * - * @package Gutenberg - * @subpackage Interactivity API - */ - -/** - * Tests for the `WP_Interactivity_Store` class. - * - * @group interactivity-api - * @covers WP_Interactivity_Store - */ -class WP_Interactivity_Store_Test extends WP_UnitTestCase { - public function set_up() { - // Clear the state before each test. - WP_Interactivity_Store::reset(); - } - - public function test_store_should_be_empty() { - $this->assertEmpty( WP_Interactivity_Store::get_data() ); - } - - public function test_store_can_be_merged() { - $data = array( - 'state' => array( - 'core' => array( - 'a' => 1, - 'b' => 2, - 'nested' => array( - 'c' => 3, - ), - ), - ), - ); - WP_Interactivity_Store::merge_data( $data ); - $this->assertSame( $data, WP_Interactivity_Store::get_data() ); - } - - public function test_store_can_be_extended() { - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'core' => array( - 'a' => 1, - ), - ), - ) - ); - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'core' => array( - 'b' => 2, - ), - 'custom' => array( - 'c' => 3, - ), - ), - ) - ); - $this->assertSame( - array( - 'state' => array( - 'core' => array( - 'a' => 1, - 'b' => 2, - ), - 'custom' => array( - 'c' => 3, - ), - ), - ), - WP_Interactivity_Store::get_data() - ); - } - - public function test_store_existing_props_should_be_overwritten() { - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'core' => array( - 'a' => 1, - ), - ), - ) - ); - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'core' => array( - 'a' => 'overwritten', - ), - ), - ) - ); - $this->assertSame( - array( - 'state' => array( - 'core' => array( - 'a' => 'overwritten', - ), - ), - ), - WP_Interactivity_Store::get_data() - ); - } - - public function test_store_existing_indexed_arrays_should_be_replaced() { - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'core' => array( - 'a' => array( 1, 2 ), - ), - ), - ) - ); - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'core' => array( - 'a' => array( 3, 4 ), - ), - ), - ) - ); - $this->assertSame( - array( - 'state' => array( - 'core' => array( - 'a' => array( 3, 4 ), - ), - ), - ), - WP_Interactivity_Store::get_data() - ); - } - - public function test_store_should_be_correctly_rendered() { - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'core' => array( - 'a' => 1, - ), - ), - ) - ); - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'core' => array( - 'b' => 2, - ), - ), - ) - ); - ob_start(); - WP_Interactivity_Store::render(); - $rendered = ob_get_clean(); - $this->assertSame( - '<script id="wp-interactivity-initial-state" type="application/json">{"state":{"core":{"a":1,"b":2}}}</script>', - $rendered - ); - } - - public function test_store_should_also_escape_tags_and_amps() { - WP_Interactivity_Store::merge_data( - array( - 'state' => array( - 'amps' => 'http://site.test/?foo=1&baz=2&bar=3', - 'tags' => 'Do not do this: <!-- <script>', - ), - ) - ); - ob_start(); - WP_Interactivity_Store::render(); - $rendered = ob_get_clean(); - $this->assertSame( - '<script id="wp-interactivity-initial-state" type="application/json">{"state":{"amps":"http:\/\/site.test\/?foo=1\u0026baz=2\u0026bar=3","tags":"Do not do this: \u003C!-- \u003Cscript\u003E"}}</script>', - $rendered - ); - } -} diff --git a/phpunit/experimental/interactivity-api/directive-processing-test.php b/phpunit/experimental/interactivity-api/directive-processing-test.php index 46ef0284df15d9..d6b5bc3860a951 100644 --- a/phpunit/experimental/interactivity-api/directive-processing-test.php +++ b/phpunit/experimental/interactivity-api/directive-processing-test.php @@ -4,81 +4,124 @@ * * @package Gutenberg * @subpackage Interactivity API - * - * @phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound */ +class Tests_Process_Directives extends WP_UnitTestCase { + public function set_up() { + parent::set_up(); -class Helper_Class { - // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - public function process_foo_test( $tags, $context ) { - } - - public function increment( $store ) { - return $store['state']['count'] + $store['context']['count']; - } + register_block_type( + 'test/context-level-1', + array( + 'render_callback' => function ( $attributes, $content ) { + return '<div data-wp-interactive=\'{ "namespace": "test" }\' data-wp-context=\'{ "myText": "level-1" }\'> <input class="level-1-input-1" data-wp-bind--value="context.myText">' . $content . '<input class="level-1-input-2" data-wp-bind--value="context.myText"></div>'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); - public static function static_increment( $store ) { - return $store['state']['count'] + $store['context']['count']; - } -} + register_block_type( + 'test/context-level-2', + array( + 'render_callback' => function ( $attributes, $content ) { + return '<div data-wp-interactive=\'{ "namespace": "test" }\' data-wp-context=\'{ "myText": "level-2" }\'><input class="level-2-input-1" data-wp-bind--value="context.myText">' . $content . '</div>'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); -function gutenberg_test_process_directives_helper_increment( $store ) { - return $store['state']['count'] + $store['context']['count']; -} + register_block_type( + 'test/context-read-only', + array( + 'render_callback' => function () { + return '<div data-wp-interactive=\'{ "namespace": "test" }\'><input class="read-only-input-1" data-wp-bind--value="context.myText"></div>'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); -/** - * Tests for the gutenberg_interactivity_process_rendered_html function. - * - * @group interactivity-api - * @covers gutenberg_interactivity_process_rendered_html - */ -class Tests_Process_Directives extends WP_UnitTestCase { - public function test_correctly_call_attribute_directive_processor_on_closing_tag() { - - // PHPUnit cannot stub functions, only classes. - $test_helper = $this->createMock( Helper_Class::class ); - - $test_helper->expects( $this->exactly( 2 ) ) - ->method( 'process_foo_test' ) - ->with( - $this->callback( - function ( $p ) { - return 'DIV' === $p->get_tag() && ( - // Either this is a closing tag... - $p->is_tag_closer() || - // ...or it is an open tag, and has the directive attribute set. - ( ! $p->is_tag_closer() && 'abc' === $p->get_attribute( 'foo-test' ) ) - ); - } - ) - ); - - $directives = array( - 'foo-test' => array( $test_helper, 'process_foo_test' ), + register_block_type( + 'test/non-interactive-with-directive', + array( + 'render_callback' => function () { + return '<input class="non-interactive-with-directive" data-wp-bind--value="context.myText">'; + }, + ) ); - $markup = '<div>Example: <div foo-test="abc"><img><span>This is a test></span><div>Here is a nested div</div></div></div>'; - $tags = new WP_Directive_Processor( $markup ); - $tags->process_rendered_html( $tags, 'foo-', $directives ); - } + register_block_type( + 'test/context-level-with-manual-inner-block-rendering', + array( + 'render_callback' => function ( $attributes, $content, $block ) { + $inner_blocks_html = ''; + foreach ( $block->inner_blocks as $inner_block ) { + $inner_blocks_html .= $inner_block->render(); + } + return '<div data-wp-interactive=\'{ "namespace": "test" }\' data-wp-context=\'{ "myText": "some value" }\'>' . $inner_blocks_html . '</div>'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); - public function test_directives_with_double_hyphen_processed_correctly() { - $test_helper = $this->createMock( Helper_Class::class ); - $test_helper->expects( $this->atLeastOnce() ) - ->method( 'process_foo_test' ); + register_block_type( + 'test/directives-ordering', + array( + 'render_callback' => function () { + return '<input data-wp-interactive=\'{ "namespace": "test" }\' data-wp-context=\'{ "isClass": true, "value": "some-value", "display": "none" }\' data-wp-bind--value="context.value" class="other-class" data-wp-class--some-class="context.isClass" data-wp-style--display="context.display">'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); - $directives = array( - 'foo-test' => array( $test_helper, 'process_foo_test' ), + register_block_type( + 'test/directives', + array( + 'render_callback' => function ( $attributes, $content ) { + $parsed_attributes = array(); + foreach ( $attributes as $key => $value ) { + $parsed_attributes[ $key ] = is_array( $value ) + ? wp_json_encode( $value, JSON_HEX_APOS ) + : esc_attr( $value ); + } + + $wrapper_attributes = get_block_wrapper_attributes( + $parsed_attributes + ); + + return "<div $wrapper_attributes>$content</div>"; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) ); - $markup = '<div foo-test--value="abc"></div>'; - $tags = new WP_Directive_Processor( $markup ); - $tags->process_rendered_html( $tags, 'foo-', $directives ); + WP_Interactivity_Initial_State::reset(); } - public function test_interactivity_process_directives_in_root_blocks() { + public function tear_down() { + WP_Interactivity_Initial_State::reset(); + unregister_block_type( 'test/context-level-1' ); + unregister_block_type( 'test/context-level-2' ); + unregister_block_type( 'test/context-read-only' ); + unregister_block_type( 'test/non-interactive-with-directive' ); + unregister_block_type( 'test/context-level-with-manual-inner-block-rendering' ); + unregister_block_type( 'test/directives-ordering' ); + unregister_block_type( 'test/directives' ); + parent::tear_down(); + } + public function test_interactivity_process_directives_in_root_blocks() { $block_content = '<!-- wp:paragraph -->' . '<p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing!</p>' . @@ -87,15 +130,11 @@ public function test_interactivity_process_directives_in_root_blocks() { '<p>Welcome to WordPress.</p>' . '<!-- /wp:paragraph -->'; - $parsed_block = parse_blocks( $block_content )[0]; - - $source_block = $parsed_block; - - $rendered_content = render_block( $parsed_block ); - + $parsed_block = parse_blocks( $block_content )[0]; + $source_block = $parsed_block; + $rendered_content = render_block( $parsed_block ); $parsed_block_second = parse_blocks( $block_content )[1]; - - $fake_parent_block = array(); + $fake_parent_block = array(); // Test that root block is intially emtpy. $this->assertEmpty( WP_Directive_Processor::$root_block ); @@ -117,40 +156,136 @@ public function test_interactivity_process_directives_in_root_blocks() { gutenberg_process_directives_in_root_blocks( $rendered_content, $parsed_block ); $this->assertEmpty( WP_Directive_Processor::$root_block ); } -} + public function test_directive_processing_of_interactive_block() { + $post_content = '<!-- wp:test/context-level-1 /-->'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_directive_processing_two_interactive_blocks_at_same_level() { + $post_content = '<!-- wp:group --><div class="wp-block-group"><!-- wp:test/context-level-1 /--><!-- wp:test/context-level-2 /--></div><!-- /wp:group -->'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-2-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-2', $value ); + } + + public function test_directives_are_processed_at_tag_end() { + $post_content = '<!-- wp:test/context-level-1 --><!-- wp:test/context-level-2 /--><!-- wp:test/context-read-only /--><!-- /wp:test/context-level-1 -->'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-2-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-2', $value ); + $p->next_tag( array( 'class_name' => 'read-only-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_non_interactive_children_of_interactive_is_rendered() { + $post_content = '<!-- wp:test/context-level-1 --><!-- wp:test/context-read-only /--><!-- wp:paragraph --><p>Welcome</p><!-- /wp:paragraph --><!-- /wp:test/context-level-1 -->'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'read-only-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag(); + $this->assertSame( 'P', $p->get_tag() ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_non_interactive_blocks_are_not_processed() { + $post_content = '<!-- wp:test/context-level-1 --><!-- wp:test/non-interactive-with-directive /--><!-- /wp:test/context-level-1 -->'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'non-interactive-with-directive' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( null, $value ); + } + + public function test_non_interactive_blocks_with_manual_inner_block_rendering_are_not_processed() { + $post_content = '<!-- wp:test/context-level-with-manual-inner-block-rendering --><!-- wp:test/non-interactive-with-directive /--><!-- /wp:test/context-level-with-manual-inner-block-rendering -->'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'non-interactive-with-directive' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( null, $value ); + } + + public function test_directives_ordering() { + $post_content = '<!-- wp:test/directives-ordering -->'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag(); + + $value = $p->get_attribute( 'class' ); + $this->assertSame( 'other-class some-class', $value ); + + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'some-value', $value ); + + $value = $p->get_attribute( 'style' ); + $this->assertSame( 'display: none;', $value ); + } -/** - * Tests for the gutenberg_interactivity_evaluate_reference function. - * - * @group interactivity-api - * @covers gutenberg_interactivity_evaluate_reference - */ -class Tests_Utils_Evaluate extends WP_UnitTestCase { public function test_evaluate_function_should_access_state() { // Init a simple store. - wp_store( + wp_initial_state( + 'test', array( - 'state' => array( - 'core' => array( - 'number' => 1, - 'bool' => true, - 'nested' => array( - 'string' => 'hi', - ), - ), + 'number' => 1, + 'bool' => true, + 'nested' => array( + 'string' => 'hi', ), ) ); - $this->assertSame( 1, gutenberg_interactivity_evaluate_reference( 'state.core.number' ) ); - $this->assertTrue( gutenberg_interactivity_evaluate_reference( 'state.core.bool' ) ); - $this->assertSame( 'hi', gutenberg_interactivity_evaluate_reference( 'state.core.nested.string' ) ); - $this->assertFalse( gutenberg_interactivity_evaluate_reference( '!state.core.bool' ) ); + + $this->assertSame( 1, gutenberg_interactivity_evaluate_reference( 'state.number', 'test' ) ); + $this->assertTrue( gutenberg_interactivity_evaluate_reference( 'state.bool', 'test' ) ); + $this->assertSame( 'hi', gutenberg_interactivity_evaluate_reference( 'state.nested.string', 'test' ) ); + $this->assertFalse( gutenberg_interactivity_evaluate_reference( '!state.bool', 'test' ) ); } public function test_evaluate_function_should_access_passed_context() { + wp_initial_state( + 'test', + array( + 'number' => 1, + 'bool' => true, + 'nested' => array( + 'string' => 'hi', + ), + ) + ); + $context = array( - 'local' => array( + 'test' => array( 'number' => 2, 'bool' => false, 'nested' => array( @@ -158,58 +293,232 @@ public function test_evaluate_function_should_access_passed_context() { ), ), ); - $this->assertSame( 2, gutenberg_interactivity_evaluate_reference( 'context.local.number', $context ) ); - $this->assertFalse( gutenberg_interactivity_evaluate_reference( 'context.local.bool', $context ) ); - $this->assertTrue( gutenberg_interactivity_evaluate_reference( '!context.local.bool', $context ) ); - $this->assertSame( 'bye', gutenberg_interactivity_evaluate_reference( 'context.local.nested.string', $context ) ); - // Previously defined state is also accessible. - $this->assertSame( 1, gutenberg_interactivity_evaluate_reference( 'state.core.number' ) ); - $this->assertTrue( gutenberg_interactivity_evaluate_reference( 'state.core.bool' ) ); - $this->assertSame( 'hi', gutenberg_interactivity_evaluate_reference( 'state.core.nested.string' ) ); + + $this->assertSame( 2, gutenberg_interactivity_evaluate_reference( 'context.number', 'test', $context ) ); + $this->assertFalse( gutenberg_interactivity_evaluate_reference( 'context.bool', 'test', $context ) ); + $this->assertTrue( gutenberg_interactivity_evaluate_reference( '!context.bool', 'test', $context ) ); + $this->assertSame( 'bye', gutenberg_interactivity_evaluate_reference( 'context.nested.string', 'test', $context ) ); + + // Defined state is also accessible. + $this->assertSame( 1, gutenberg_interactivity_evaluate_reference( 'state.number', 'test' ) ); + $this->assertTrue( gutenberg_interactivity_evaluate_reference( 'state.bool', 'test' ) ); + $this->assertSame( 'hi', gutenberg_interactivity_evaluate_reference( 'state.nested.string', 'test' ) ); } public function test_evaluate_function_should_return_null_for_unresolved_paths() { - $this->assertNull( gutenberg_interactivity_evaluate_reference( 'this.property.doesnt.exist' ) ); + $this->assertNull( gutenberg_interactivity_evaluate_reference( 'this.property.doesnt.exist', 'myblock' ) ); } public function test_evaluate_function_should_execute_anonymous_functions() { - $context = new WP_Directive_Context( array( 'count' => 2 ) ); - $helper = new Helper_Class(); + $this->markTestSkipped( 'Derived state was supported for `wp_store()` but not for `wp_initial_state()` yet.' ); + + $context = new WP_Directive_Context( array( 'myblock' => array( 'count' => 2 ) ) ); - wp_store( + wp_initial_state( + 'myblock', array( - 'state' => array( - 'count' => 3, - ), - 'selectors' => array( - 'anonymous_function' => function ( $store ) { - return $store['state']['count'] + $store['context']['count']; - }, - // Other types of callables should not be executed. - 'function_name' => 'gutenberg_test_process_directives_helper_increment', - 'class_method' => array( $helper, 'increment' ), - 'class_static_method' => 'Helper_Class::static_increment', - 'class_static_method_as_array' => array( 'Helper_Class', 'static_increment' ), - ), + 'count' => 3, + 'anonymous_function' => function ( $store ) { + return $store['state']['count'] + $store['context']['count']; + }, + // Other types of callables should not be executed. + 'function_name' => 'gutenberg_test_process_directives_helper_increment', + 'class_method' => array( $this, 'increment' ), + 'class_static_method' => array( 'Tests_Process_Directives', 'static_increment' ), ) ); - $this->assertSame( 5, gutenberg_interactivity_evaluate_reference( 'selectors.anonymous_function', $context->get_context() ) ); + $this->assertSame( 5, gutenberg_interactivity_evaluate_reference( 'state.anonymous_function', 'myblock', $context->get_context() ) ); $this->assertSame( 'gutenberg_test_process_directives_helper_increment', - gutenberg_interactivity_evaluate_reference( 'selectors.function_name', $context->get_context() ) - ); - $this->assertSame( - array( $helper, 'increment' ), - gutenberg_interactivity_evaluate_reference( 'selectors.class_method', $context->get_context() ) + gutenberg_interactivity_evaluate_reference( 'state.function_name', 'myblock', $context->get_context() ) ); $this->assertSame( - 'Helper_Class::static_increment', - gutenberg_interactivity_evaluate_reference( 'selectors.class_static_method', $context->get_context() ) + array( $this, 'increment' ), + gutenberg_interactivity_evaluate_reference( 'state.class_method', 'myblock', $context->get_context() ) ); $this->assertSame( - array( 'Helper_Class', 'static_increment' ), - gutenberg_interactivity_evaluate_reference( 'selectors.class_static_method_as_array', $context->get_context() ) + array( 'Tests_Process_Directives', 'static_increment' ), + gutenberg_interactivity_evaluate_reference( 'state.class_static_method', 'myblock', $context->get_context() ) ); } + + public function test_namespace_should_be_inherited_from_ancestor() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state' ) ); + + $post_content = ' + <!-- wp:test/directives { "data-wp-interactive": { "namespace": "test-1" } } --> + <!-- wp:test/directives { "data-wp-context": { "text": "context" } } --> + <!-- wp:test/directives { + "class": "bind-state", + "data-wp-bind--data-value": "state.text" + } /--> + <!-- wp:test/directives { + "class": "bind-context", + "data-wp-bind--data-value": "context.text" + } /--> + <!-- /wp:test/directives --> + <!-- /wp:test/directives --> + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'bind-state' ) ); + $this->assertSame( 'state', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'bind-context' ) ); + $this->assertSame( 'context', $tags->get_attribute( 'data-value' ) ); + } + + public function test_namespace_should_be_inherited_from_same_element() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + <!-- wp:test/directives { "data-wp-interactive": { "namespace": "test-1" } } --> + <!-- wp:test/directives { "data-wp-context": { "text": "context" } } --> + <!-- wp:test/directives { + "class": "bind-state", + "data-wp-interactive": { "namespace": "test-2" }, + "data-wp-bind--data-value": "state.text" + } /--> + <!-- wp:test/directives { + "class": "bind-context", + "data-wp-interactive": { "namespace": "test-2" }, + "data-wp-context": { "text": "context-2" }, + "data-wp-bind--data-value": "context.text" + } /--> + <!-- /wp:test/directives --> + <!-- /wp:test/directives --> + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'bind-state' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'bind-context' ) ); + $this->assertSame( 'context-2', $tags->get_attribute( 'data-value' ) ); + } + + public function test_namespace_should_not_leak_from_descendant() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + <!-- wp:test/directives { + "data-wp-interactive": { "namespace": "test-2" }, + "data-wp-context": { "text": "context-2" } + } --> + <!-- wp:test/directives { + "class": "target", + "data-wp-interactive": { "namespace": "test-1" }, + "data-wp-context": { "text": "context-1" }, + "data-wp-bind--data-state": "state.text", + "data-wp-bind--data-context": "context.text" + } --> + <!-- wp:test/directives { + "data-wp-interactive": { "namespace": "test-2" } + } /--> + <!-- /wp:test/directives --> + <!-- /wp:test/directives --> + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'target' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-state' ) ); + $this->assertSame( 'context-1', $tags->get_attribute( 'data-context' ) ); + } + + public function test_namespace_should_not_leak_from_sibling() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + <!-- wp:test/directives { + "data-wp-interactive": { "namespace": "test-2" }, + "data-wp-context": { "text": "context-2" } + } --> + <!-- wp:test/directives { + "data-wp-interactive": { "namespace": "test-1" }, + "data-wp-context": { "text": "context-1" } + } --> + <!-- wp:test/directives { + "data-wp-interactive": { "namespace": "test-2" } + } /--> + <!-- wp:test/directives { + "class": "target", + "data-wp-bind--data-from-state": "state.text", + "data-wp-bind--data-from-context": "context.text" + } /--> + <!-- /wp:test/directives --> + <!-- /wp:test/directives --> + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'target' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-from-state' ) ); + $this->assertSame( 'context-1', $tags->get_attribute( 'data-from-context' ) ); + } + + public function test_namespace_can_be_overwritten_in_directives() { + /* + * This function call should be done inside block render functions. We + * run it here instead just for conveninence. + */ + wp_initial_state( 'test-1', array( 'text' => 'state-1' ) ); + wp_initial_state( 'test-2', array( 'text' => 'state-2' ) ); + + $post_content = ' + <!-- wp:test/directives { "data-wp-interactive": { "namespace": "test-1" } } --> + <!-- wp:test/directives { + "class": "inherited-ns", + "data-wp-bind--data-value": "state.text" + } /--> + <!-- wp:test/directives { + "class": "custom-ns", + "data-wp-bind--data-value": "test-2::state.text" + } /--> + <!-- wp:test/directives { + "class": "mixed-ns", + "data-wp-bind--data-inherited-ns": "state.text", + "data-wp-bind--data-custom-ns": "test-2::state.text" + } /--> + <!-- /wp:test/directives --> + '; + + $html = do_blocks( $post_content ); + $tags = new WP_HTML_Tag_Processor( $html ); + + $tags->next_tag( array( 'class_name' => 'inherited-ns' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'custom-ns' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-value' ) ); + + $tags->next_tag( array( 'class_name' => 'mixed-ns' ) ); + $this->assertSame( 'state-1', $tags->get_attribute( 'data-inherited-ns' ) ); + $this->assertSame( 'state-2', $tags->get_attribute( 'data-custom-ns' ) ); + } } diff --git a/phpunit/experimental/interactivity-api/directives/wp-bind-test.php b/phpunit/experimental/interactivity-api/directives/wp-bind-test.php index bfb4c428cd9466..8fe212bb8ed93a 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-bind-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-bind-test.php @@ -14,16 +14,17 @@ */ class Tests_Directives_WpBind extends WP_UnitTestCase { public function test_directive_sets_attribute() { - $markup = '<img data-wp-bind--src="context.myblock.imageSource" />'; + $markup = '<img data-wp-bind--src="context.imageSource" />'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'imageSource' => './wordpress.png' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_bind( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_bind( $tags, $context, $directive_ns ); $this->assertSame( - '<img src="./wordpress.png" data-wp-bind--src="context.myblock.imageSource" />', + '<img src="./wordpress.png" data-wp-bind--src="context.imageSource" />', $tags->get_updated_html() ); $this->assertSame( './wordpress.png', $tags->get_attribute( 'src' ) ); @@ -31,13 +32,14 @@ public function test_directive_sets_attribute() { } public function test_directive_ignores_empty_bound_attribute() { - $markup = '<img data-wp-bind.="context.myblock.imageSource" />'; + $markup = '<img data-wp-bind.="context.imageSource" />'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'imageSource' => './wordpress.png' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_bind( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_bind( $tags, $context, $directive_ns ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertNull( $tags->get_attribute( 'src' ) ); diff --git a/phpunit/experimental/interactivity-api/directives/wp-class-test.php b/phpunit/experimental/interactivity-api/directives/wp-class-test.php index 419546c6d9ef8b..f40486647ff8b8 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-class-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-class-test.php @@ -14,16 +14,17 @@ */ class Tests_Directives_WpClass extends WP_UnitTestCase { public function test_directive_adds_class() { - $markup = '<div data-wp-class--red="context.myblock.isRed" class="blue">Test</div>'; + $markup = '<div data-wp-class--red="context.isRed" class="blue">Test</div>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isRed' => true ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '<div data-wp-class--red="context.myblock.isRed" class="blue red">Test</div>', + '<div data-wp-class--red="context.isRed" class="blue red">Test</div>', $tags->get_updated_html() ); $this->assertStringContainsString( 'red', $tags->get_attribute( 'class' ) ); @@ -31,16 +32,17 @@ public function test_directive_adds_class() { } public function test_directive_removes_class() { - $markup = '<div data-wp-class--blue="context.myblock.isBlue" class="red blue">Test</div>'; + $markup = '<div data-wp-class--blue="context.isBlue" class="red blue">Test</div>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '<div data-wp-class--blue="context.myblock.isBlue" class="red">Test</div>', + '<div data-wp-class--blue="context.isBlue" class="red">Test</div>', $tags->get_updated_html() ); $this->assertStringNotContainsString( 'blue', $tags->get_attribute( 'class' ) ); @@ -48,17 +50,18 @@ public function test_directive_removes_class() { } public function test_directive_removes_empty_class_attribute() { - $markup = '<div data-wp-class--blue="context.myblock.isBlue" class="blue">Test</div>'; + $markup = '<div data-wp-class--blue="context.isBlue" class="blue">Test</div>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( // WP_HTML_Tag_Processor has a TODO note to prune whitespace after classname removal. - '<div data-wp-class--blue="context.myblock.isBlue" >Test</div>', + '<div data-wp-class--blue="context.isBlue" >Test</div>', $tags->get_updated_html() ); $this->assertNull( $tags->get_attribute( 'class' ) ); @@ -66,16 +69,17 @@ public function test_directive_removes_empty_class_attribute() { } public function test_directive_does_not_remove_non_existant_class() { - $markup = '<div data-wp-class--blue="context.myblock.isBlue" class="green red">Test</div>'; + $markup = '<div data-wp-class--blue="context.isBlue" class="green red">Test</div>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isBlue' => false ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( - '<div data-wp-class--blue="context.myblock.isBlue" class="green red">Test</div>', + '<div data-wp-class--blue="context.isBlue" class="green red">Test</div>', $tags->get_updated_html() ); $this->assertSame( 'green red', $tags->get_attribute( 'class' ) ); @@ -83,13 +87,14 @@ public function test_directive_does_not_remove_non_existant_class() { } public function test_directive_ignores_empty_class_name() { - $markup = '<div data-wp-class.="context.myblock.isRed" class="blue">Test</div>'; + $markup = '<div data-wp-class.="context.isRed" class="blue">Test</div>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'isRed' => true ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_class( $tags, $context ); + $directive_ns = 'myblock'; + gutenberg_interactivity_process_wp_class( $tags, $context, $directive_ns ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertStringNotContainsString( 'red', $tags->get_attribute( 'class' ) ); diff --git a/phpunit/experimental/interactivity-api/directives/wp-context-test.php b/phpunit/experimental/interactivity-api/directives/wp-context-test.php index 1277b016848cce..788feec95fe7c5 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-context-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-context-test.php @@ -21,11 +21,12 @@ public function test_directive_merges_context_correctly_upon_wp_context_attribut ) ); - $markup = '<div data-wp-context=\'{ "myblock": { "open": true } }\'>'; + $ns = 'myblock'; + $markup = '<div data-wp-context=\'{ "open": true }\'>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, $ns ); $this->assertSame( array( @@ -38,39 +39,39 @@ public function test_directive_merges_context_correctly_upon_wp_context_attribut public function test_directive_resets_context_correctly_upon_closing_tag() { $context = new WP_Directive_Context( - array( 'my-key' => 'original-value' ) + array( 'myblock' => array( 'my-key' => 'original-value' ) ) ); $context->set_context( - array( 'my-key' => 'new-value' ) + array( 'myblock' => array( 'my-key' => 'new-value' ) ) ); $markup = '</div>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'original-value' ), - $context->get_context() + $context->get_context()['myblock'] ); } public function test_directive_doesnt_throw_on_malformed_context_objects() { $context = new WP_Directive_Context( - array( 'my-key' => 'some-value' ) + array( 'myblock' => array( 'my-key' => 'some-value' ) ) ); $markup = '<div data-wp-context=\'{ "wrong_json_object: }\'>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); } @@ -87,36 +88,36 @@ public function test_directive_keeps_working_after_malformed_context_objects() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( @@ -138,36 +139,36 @@ public function test_directive_keeps_working_with_a_directive_without_value() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( @@ -189,36 +190,36 @@ public function test_directive_keeps_working_with_an_empty_directive() { // Parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing children div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Still the same context. $this->assertSame( array( 'my-key' => 'some-value' ), - $context->get_context() + $context->get_context()['myblock'] ); // Closing parent div. $tags->next_tag( array( 'tag_closers' => 'visit' ) ); - gutenberg_interactivity_process_wp_context( $tags, $context ); + gutenberg_interactivity_process_wp_context( $tags, $context, 'myblock' ); // Now the context is empty. $this->assertSame( diff --git a/phpunit/experimental/interactivity-api/directives/wp-style-test.php b/phpunit/experimental/interactivity-api/directives/wp-style-test.php index 8942559b2fe89f..9625803ebca78f 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-style-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-style-test.php @@ -14,16 +14,16 @@ */ class Tests_Directives_WpStyle extends WP_UnitTestCase { public function test_directive_adds_style() { - $markup = '<div data-wp-style--color="context.myblock.color" style="background: blue;">Test</div>'; + $markup = '<div data-wp-style--color="context.color" style="background: blue;">Test</div>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_style( $tags, $context ); + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); $this->assertSame( - '<div data-wp-style--color="context.myblock.color" style="background: blue;color: green;">Test</div>', + '<div data-wp-style--color="context.color" style="background: blue;color: green;">Test</div>', $tags->get_updated_html() ); $this->assertStringContainsString( 'color: green;', $tags->get_attribute( 'style' ) ); @@ -31,16 +31,33 @@ public function test_directive_adds_style() { } public function test_directive_ignores_empty_style() { - $markup = '<div data-wp-style.="context.myblock.color" style="background: blue;">Test</div>'; + $markup = '<div data-wp-style="context.color" style="background: blue;">Test</div>'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); $context = $context_before; - gutenberg_interactivity_process_wp_style( $tags, $context ); + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); $this->assertSame( $markup, $tags->get_updated_html() ); $this->assertStringNotContainsString( 'color: green;', $tags->get_attribute( 'style' ) ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-style directive changed context' ); } + + public function test_directive_works_without_style_attribute() { + $markup = '<div data-wp-style--color="context.color">Test</div>'; + $tags = new WP_HTML_Tag_Processor( $markup ); + $tags->next_tag(); + + $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); + $context = $context_before; + gutenberg_interactivity_process_wp_style( $tags, $context, 'myblock' ); + + $this->assertSame( + '<div style="color: green;" data-wp-style--color="context.color">Test</div>', + $tags->get_updated_html() + ); + $this->assertSame( 'color: green;', $tags->get_attribute( 'style' ) ); + $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-style directive changed context' ); + } } diff --git a/phpunit/experimental/interactivity-api/directives/wp-text-test.php b/phpunit/experimental/interactivity-api/directives/wp-text-test.php index 81d2d0f370a64b..9c889a3f0eb68f 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-text-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-text-test.php @@ -14,31 +14,31 @@ */ class Tests_Directives_WpText extends WP_UnitTestCase { public function test_directive_sets_inner_html_based_on_attribute_value_and_escapes_html() { - $markup = '<div data-wp-text="context.myblock.someText"></div>'; + $markup = '<div data-wp-text="context.someText"></div>'; $tags = new WP_Directive_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'someText' => 'The HTML tag <br> produces a line break.' ) ) ); $context = clone $context_before; - gutenberg_interactivity_process_wp_text( $tags, $context ); + gutenberg_interactivity_process_wp_text( $tags, $context, 'myblock' ); - $expected_markup = '<div data-wp-text="context.myblock.someText">The HTML tag <br> produces a line break.</div>'; + $expected_markup = '<div data-wp-text="context.someText">The HTML tag <br> produces a line break.</div>'; $this->assertSame( $expected_markup, $tags->get_updated_html() ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-text directive changed context' ); } public function test_directive_overwrites_inner_html_based_on_attribute_value() { - $markup = '<div data-wp-text="context.myblock.someText">Lorem ipsum dolor sit.</div>'; + $markup = '<div data-wp-text="context.someText">Lorem ipsum dolor sit.</div>'; $tags = new WP_Directive_Processor( $markup ); $tags->next_tag(); $context_before = new WP_Directive_Context( array( 'myblock' => array( 'someText' => 'Honi soit qui mal y pense.' ) ) ); $context = clone $context_before; - gutenberg_interactivity_process_wp_text( $tags, $context ); + gutenberg_interactivity_process_wp_text( $tags, $context, 'myblock' ); - $expected_markup = '<div data-wp-text="context.myblock.someText">Honi soit qui mal y pense.</div>'; + $expected_markup = '<div data-wp-text="context.someText">Honi soit qui mal y pense.</div>'; $this->assertSame( $expected_markup, $tags->get_updated_html() ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-text directive changed context' ); } diff --git a/phpunit/style-engine/style-engine-test.php b/phpunit/style-engine/style-engine-test.php index d8ded749c640b2..b66ccf5694bd66 100644 --- a/phpunit/style-engine/style-engine-test.php +++ b/phpunit/style-engine/style-engine-test.php @@ -506,18 +506,22 @@ public function data_wp_style_engine_get_styles() { 'inline_background_image_url_with_background_size' => array( 'block_styles' => array( 'background' => array( - 'backgroundImage' => array( + 'backgroundImage' => array( 'url' => 'https://example.com/image.jpg', ), - 'backgroundSize' => 'cover', + 'backgroundPosition' => 'center', + 'backgroundRepeat' => 'no-repeat', + 'backgroundSize' => 'cover', ), ), 'options' => array(), 'expected_output' => array( - 'css' => "background-image:url('https://example.com/image.jpg');background-size:cover;", + 'css' => "background-image:url('https://example.com/image.jpg');background-position:center;background-repeat:no-repeat;background-size:cover;", 'declarations' => array( - 'background-image' => "url('https://example.com/image.jpg')", - 'background-size' => 'cover', + 'background-image' => "url('https://example.com/image.jpg')", + 'background-position' => 'center', + 'background-repeat' => 'no-repeat', + 'background-size' => 'cover', ), ), ), diff --git a/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php b/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php deleted file mode 100644 index e30c199612b8a9..00000000000000 --- a/phpunit/tests/fonts/font-library/wpFontFamilyUtils/hasFontMimeType.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Test WP_Font_Family_Utils::has_font_mime_type(). - * - * @package WordPress - * @subpackage Font Library - * - * @group fonts - * @group font-library - * - * @covers WP_Font_Family_Utils::has_font_mime_type - */ -class Tests_Fonts_WpFontsFamilyUtils_HasFontMimeType extends WP_UnitTestCase { - - /** - * @dataProvider data_should_succeed_when_has_mime_type - * - * @param string $font_file Font file path. - */ - public function test_should_succeed_when_has_mime_type( $font_file ) { - $this->assertTrue( WP_Font_Family_Utils::has_font_mime_type( $font_file ) ); - } - - /** - * Data provider. - * - * @return array[] - */ - public function data_should_succeed_when_has_mime_type() { - return array( - 'ttf' => array( '/temp/piazzolla_400_italic.ttf' ), - 'otf' => array( '/temp/piazzolla_400_italic.otf' ), - 'woff' => array( '/temp/piazzolla_400_italic.woff' ), - 'woff2' => array( '/temp/piazzolla_400_italic.woff2' ), - ); - } - - /** - * @dataProvider data_should_fail_when_mime_not_supported - * - * @param string $font_file Font file path. - */ - public function test_should_fail_when_mime_not_supported( $font_file ) { - $this->assertFalse( WP_Font_Family_Utils::has_font_mime_type( $font_file ) ); - } - - /** - * Data provider. - * - * @return array[] - */ - public function data_should_fail_when_mime_not_supported() { - return array( - 'exe' => array( '/temp/test.exe' ), - 'md' => array( '/temp/license.md' ), - 'php' => array( '/temp/test.php' ), - 'txt' => array( '/temp/test.txt' ), - 'zip' => array( '/temp/lato.zip' ), - ); - } -} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/base.php b/phpunit/tests/fonts/font-library/wpFontLibrary/base.php new file mode 100644 index 00000000000000..e8d970f5b3d393 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/base.php @@ -0,0 +1,26 @@ +<?php +/** + * Test Case for WP_Font_Library tests. + * + * @package WordPress + * @subpackage Font Library + */ +abstract class WP_Font_Library_UnitTestCase extends WP_UnitTestCase { + public function reset_font_collections() { + // Resets the private static property WP_Font_Library::$collections to empty array. + $reflection = new ReflectionClass( 'WP_Font_Library' ); + $property = $reflection->getProperty( 'collections' ); + $property->setAccessible( true ); + $property->setValue( array() ); + } + + public function set_up() { + parent::set_up(); + $this->reset_font_collections(); + } + + public function tear_down() { + parent::tear_down(); + $this->reset_font_collections(); + } +} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php index bfdb7258fa11aa..00d5ca2dcb2e73 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -10,20 +10,16 @@ * * @covers WP_Font_Library::get_font_collection */ -class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_UnitTestCase { +class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_Font_Library_UnitTestCase { - public static function set_up_before_class() { + public function test_should_get_font_collection() { $my_font_collection_config = array( 'id' => 'my-font-collection', 'name' => 'My Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), ); - wp_register_font_collection( $my_font_collection_config ); - } - - public function test_should_get_font_collection() { $font_collection = WP_Font_Library::get_font_collection( 'my-font-collection' ); $this->assertInstanceOf( 'WP_Font_Collection', $font_collection ); } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php index 97e66e64e87161..40eacba8e18c56 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -10,11 +10,13 @@ * * @covers WP_Font_Library::get_font_collections */ -class Tests_Fonts_WpFontLibrary_GetFontCollections extends WP_UnitTestCase { - - public static function set_up_before_class() { - $font_library = new WP_Font_Library(); +class Tests_Fonts_WpFontLibrary_GetFontCollections extends WP_Font_Library_UnitTestCase { + public function test_should_get_an_empty_list() { + $font_collections = WP_Font_Library::get_font_collections(); + $this->assertEmpty( $font_collections, 'Should return an empty array.' ); + } + public function test_should_get_mock_font_collection() { $my_font_collection_config = array( 'id' => 'my-font-collection', 'name' => 'My Font Collection', @@ -22,23 +24,11 @@ public static function set_up_before_class() { 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), ); - $font_library::register_font_collection( $my_font_collection_config ); - } - - public function test_should_get_the_default_font_collection() { - $font_collections = WP_Font_Library::get_font_collections(); - $this->assertArrayHasKey( 'default-font-collection', $font_collections, 'Default Google Fonts collection should be registered' ); - $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['default-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' ); - } + WP_Font_Library::register_font_collection( $my_font_collection_config ); - public function test_should_get_the_right_number_of_collections() { $font_collections = WP_Font_Library::get_font_collections(); $this->assertNotEmpty( $font_collections, 'Sould return an array of font collections.' ); - $this->assertCount( 2, $font_collections, 'Should return an array with one font collection.' ); - } - - public function test_should_get_mock_font_collection() { - $font_collections = WP_Font_Library::get_font_collections(); + $this->assertCount( 1, $font_collections, 'Should return an array with one font collection.' ); $this->assertArrayHasKey( 'my-font-collection', $font_collections, 'The array should have the key of the registered font collection id.' ); $this->assertInstanceOf( 'WP_Font_Collection', $font_collections['my-font-collection'], 'The value of the array $font_collections[id] should be an instance of WP_Font_Collection class.' ); } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php index 4bbafc55a2147c..1200200d7160b2 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontsDir.php @@ -10,7 +10,7 @@ * * @covers WP_Font_Library::get_fonts_dir */ -class Tests_Fonts_WpFontLibrary_GetFontsDir extends WP_UnitTestCase { +class Tests_Fonts_WpFontLibrary_GetFontsDir extends WP_Font_Library_UnitTestCase { public function test_get_fonts_dir() { $this->assertStringEndsWith( '/wp-content/fonts', WP_Font_Library::get_fonts_dir() ); diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php index 708134af69e92a..485587060f16a1 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getMimeTypes.php @@ -10,7 +10,7 @@ * * @covers WP_Font_Family_Utils::get_expected_font_mime_types_per_php_version */ -class Tests_Fonts_WpFontsFamilyUtils_GetMimeTypes extends WP_UnitTestCase { +class Tests_Fonts_WpFontsFamilyUtils_GetMimeTypes extends WP_Font_Library_UnitTestCase { /** * diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php index 6bc5fbb8161cee..2569830f6bf2aa 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -10,7 +10,7 @@ * * @covers WP_Font_Library::register_font_collection */ -class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_UnitTestCase { +class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_Font_Library_UnitTestCase { public function test_should_register_font_collection() { $config = array( @@ -70,8 +70,10 @@ public function test_should_return_error_if_id_is_repeated() { $collection1 = WP_Font_Library::register_font_collection( $config1 ); $this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' ); + // Expects a _doing_it_wrong notice. + $this->setExpectedIncorrectUsage( 'WP_Font_Library::register_font_collection' ); // Try to register a second collection with same id. $collection2 = WP_Font_Library::register_font_collection( $config2 ); - $this->assertWPError( $collection2, 'Second collection with the same id should fail.' ); + $this->assertWPError( $collection2, 'A WP_Error should be returned.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php b/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php index daa4c84aad9004..29d481d8afd6bc 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/setUploadDir.php @@ -10,7 +10,7 @@ * * @covers WP_Font_Library::set_upload_dir */ -class Tests_Fonts_WpFontLibrary_SetUploadDir extends WP_UnitTestCase { +class Tests_Fonts_WpFontLibrary_SetUploadDir extends WP_Font_Library_UnitTestCase { public function test_should_set_fonts_upload_dir() { $defaults = array( diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php new file mode 100644 index 00000000000000..e6e16956814fb4 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php @@ -0,0 +1,54 @@ +<?php +/** + * Test WP_Font_Library::unregister_font_collection(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Library::unregister_font_collection + */ +class Tests_Fonts_WpFontLibrary_UnregisterFontCollection extends WP_Font_Library_UnitTestCase { + + public function test_should_unregister_font_collection() { + // Registers two mock font collections. + $config = array( + 'id' => 'mock-font-collection-1', + 'name' => 'Mock Collection to be unregistered', + 'description' => 'A mock font collection to be unregistered.', + 'src' => 'my-collection-data.json', + ); + WP_Font_Library::register_font_collection( $config ); + + $config = array( + 'id' => 'mock-font-collection-2', + 'name' => 'Mock Collection', + 'description' => 'A mock font collection.', + 'src' => 'my-mock-data.json', + ); + WP_Font_Library::register_font_collection( $config ); + + // Unregister mock font collection. + WP_Font_Library::unregister_font_collection( 'mock-font-collection-1' ); + $collections = WP_Font_Library::get_font_collections(); + $this->assertArrayNotHasKey( 'mock-font-collection-1', $collections, 'Font collection was not unregistered.' ); + $this->assertArrayHasKey( 'mock-font-collection-2', $collections, 'Font collection was unregistered by mistake.' ); + + // Unregisters remaining mock font collection. + WP_Font_Library::unregister_font_collection( 'mock-font-collection-2' ); + $collections = WP_Font_Library::get_font_collections(); + $this->assertArrayNotHasKey( 'mock-font-collection-2', $collections, 'Mock font collection was not unregistered.' ); + + // Checks that all font collections were unregistered. + $this->assertEmpty( $collections, 'Font collections were not unregistered.' ); + } + + public function unregister_non_existing_collection() { + // Unregisters non existing font collection. + WP_Font_Library::unregister_font_collection( 'non-existing-collection' ); + $collections = WP_Font_Library::get_font_collections(); + $this->assertEmpty( $collections, 'Should not be registered collections.' ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php index d35022306f4e6f..98c1cb6e13fe5c 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php @@ -21,10 +21,10 @@ class Tests_Fonts_WPRESTFontFamiliesController_InstallFonts extends WP_REST_Font * @param array $files Font files to install. * @param array $expected_response Expected response data. */ - public function test_install_fonts( $font_families, $files, $expected_response ) { - $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); - $font_families_json = json_encode( $font_families ); - $install_request->set_param( 'font_families', $font_families_json ); + public function test_install_fonts( $font_family_settings, $files, $expected_response ) { + $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $font_family_json = json_encode( $font_family_settings ); + $install_request->set_param( 'font_family_settings', $font_family_json ); $install_request->set_file_params( $files ); $response = rest_get_server()->dispatch( $install_request ); $data = $response->get_data(); @@ -68,38 +68,22 @@ public function data_install_fonts() { return array( 'google_fonts_to_download' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', ), ), ), - 'files' => array(), - 'expected_response' => array( + 'files' => array(), + 'expected_response' => array( 'successes' => array( array( 'fontFamily' => 'Piazzolla', @@ -114,55 +98,27 @@ public function data_install_fonts() { ), ), ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', - ), - ), - ), ), 'errors' => array(), ), ), 'google_fonts_to_use_as_is' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', ), ), ), - 'files' => array(), - 'expected_response' => array( + 'files' => array(), + 'expected_response' => array( 'successes' => array( array( 'fontFamily' => 'Piazzolla', @@ -177,35 +133,19 @@ public function data_install_fonts() { ), ), ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => 'http://fonts.gstatic.com/s/montserrat/v25/JTUHjIg1_i6t8kCHKm4532VJOt5-QNFgpCtr6Uw-Y3tcoqK5.ttf', - - ), - ), - ), ), 'errors' => array(), ), ), 'fonts_without_font_faces' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Arial', - 'slug' => 'arial', - 'name' => 'Arial', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Arial', + 'slug' => 'arial', + 'name' => 'Arial', ), - 'files' => array(), - 'expected_response' => array( + 'files' => array(), + 'expected_response' => array( 'successes' => array( array( 'fontFamily' => 'Arial', @@ -218,35 +158,20 @@ public function data_install_fonts() { ), 'fonts_with_local_fonts_assets' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files0', - ), - ), - ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'uploadedFile' => 'files1', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files0', ), ), ), - 'files' => array( + 'files' => array( 'files0' => array( 'name' => 'piazzola1.ttf', 'type' => 'font/ttf', @@ -262,7 +187,7 @@ public function data_install_fonts() { 'size' => 123, ), ), - 'expected_response' => array( + 'expected_response' => array( 'successes' => array( array( 'fontFamily' => 'Piazzolla', @@ -277,20 +202,6 @@ public function data_install_fonts() { ), ), ), - array( - 'fontFamily' => 'Montserrat', - 'slug' => 'montserrat', - 'name' => 'Montserrat', - 'fontFace' => array( - array( - 'fontFamily' => 'Montserrat', - 'fontStyle' => 'normal', - 'fontWeight' => '100', - 'src' => '/wp-content/fonts/montserrat_normal_100.ttf', - ), - ), - ), - ), 'errors' => array(), ), @@ -325,15 +236,15 @@ public function data_install_with_improper_inputs() { return array( 'not a font families array' => array( - 'font_families' => 'This is not an array', + 'font_family_settings' => 'This is not an array', ), 'empty array' => array( - 'font_families' => array(), + 'font_family_settings' => array(), ), 'without slug' => array( - 'font_families' => array( + 'font_family_settings' => array( array( 'fontFamily' => 'Piazzolla', 'name' => 'Piazzolla', @@ -342,63 +253,55 @@ public function data_install_with_improper_inputs() { ), 'with improper font face property' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => 'This is not an array', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => 'This is not an array', ), ), 'with empty font face property' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array(), - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array(), ), ), 'fontface referencing uploaded file without uploaded files' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files0', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files0', ), ), ), - 'files' => array(), + 'files' => array(), ), 'fontface referencing uploaded file without uploaded files' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'name' => 'Piazzolla', - 'slug' => 'piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'uploadedFile' => 'files666', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'uploadedFile' => 'files666', ), ), ), - 'files' => array( + 'files' => array( 'files0' => array( 'name' => 'piazzola1.ttf', 'type' => 'font/ttf', @@ -410,20 +313,18 @@ public function data_install_with_improper_inputs() { ), 'fontface with incompatible properties (downloadFromUrl and uploadedFile together)' => array( - 'font_families' => array( - array( - 'fontFamily' => 'Piazzolla', - 'slug' => 'piazzolla', - 'name' => 'Piazzolla', - 'fontFace' => array( - array( - 'fontFamily' => 'Piazzolla', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', - 'uploadedFile' => 'files0', - ), + 'font_family_settings' => array( + 'fontFamily' => 'Piazzolla', + 'slug' => 'piazzolla', + 'name' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'downloadFromUrl' => 'http://fonts.gstatic.com/s/piazzolla/v33/N0b72SlTPu5rIkWIZjVgI-TckS03oGpPETyEJ88Rbvi0_TzOzKcQhZqx3gX9BRy5m5M.ttf', + 'uploadedFile' => 'files0', ), ), ), diff --git a/schemas/json/block.json b/schemas/json/block.json index 7e0c8715a4abda..fd69ea1badb339 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -344,6 +344,11 @@ "description": "By default, all blocks will appear in the inserter, block transforms menu, Style Book, etc. To hide a block from all parts of the user interface so that it can only be inserted programmatically, set inserter to false.", "default": true }, + "renaming": { + "type": "boolean", + "description": "By default, a block can be renamed by a user from the block 'Options' dropdown or the 'Advanced' panel. To disable this behavior, set renaming to false.", + "default": true + }, "layout": { "default": false, "description": "This value only applies to blocks that are containers for inner blocks. If set to `true` the layout type will be `flow`. For other layout types it's necessary to set the `type` explicitly inside the `default` object.", diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index adeabc860c8342..9080a6dc194021 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -696,7 +696,9 @@ test.describe( 'Image', () => { await expect( linkDom ).toHaveAttribute( 'href', url ); } ); - test( 'should upload external image', async ( { editor } ) => { + test( 'should upload external image to media library', async ( { + editor, + } ) => { await editor.insertBlock( { name: 'core/image', attributes: { @@ -704,7 +706,7 @@ test.describe( 'Image', () => { }, } ); - await editor.clickBlockToolbarButton( 'Upload external image' ); + await editor.clickBlockToolbarButton( 'Upload image to media library' ); const imageBlock = editor.canvas.locator( 'role=document[name="Block: Image"i]' diff --git a/test/e2e/specs/editor/blocks/links.spec.js b/test/e2e/specs/editor/blocks/links.spec.js index d989a2d7e5559a..9620b45fed9da8 100644 --- a/test/e2e/specs/editor/blocks/links.spec.js +++ b/test/e2e/specs/editor/blocks/links.spec.js @@ -1228,10 +1228,10 @@ class LinkUtils { await this.page.evaluate( ( _isFixed ) => { const { select, dispatch } = window.wp.data; const isCurrentlyFixed = - select( 'core/edit-post' ).isFeatureActive( 'fixedToolbar' ); + select( 'core/preferences' ).get( 'fixedToolbar' ); if ( isCurrentlyFixed !== _isFixed ) { - dispatch( 'core/edit-post' ).toggleFeature( 'fixedToolbar' ); + dispatch( 'core/preferences' ).toggle( 'fixedToolbar' ); } }, isFixed ); } diff --git a/test/e2e/specs/editor/blocks/missing.spec.js b/test/e2e/specs/editor/blocks/missing.spec.js index 69c26c3f858762..9186bac17f3484 100644 --- a/test/e2e/specs/editor/blocks/missing.spec.js +++ b/test/e2e/specs/editor/blocks/missing.spec.js @@ -9,6 +9,7 @@ test.describe( 'missing block', () => { } ); test( 'should strip potentially malicious on* attributes', async ( { + editor, page, } ) => { let hasAlert = false; @@ -17,12 +18,9 @@ test.describe( 'missing block', () => { hasAlert = true; } ); - await page.evaluate( () => { - const block = window.wp.blocks.parse( - `<!-- wp:non-existing-block-here --><img src onerror=alert(1)>` - ); - window.wp.data.dispatch( 'core/block-editor' ).resetBlocks( block ); - } ); + await editor.setContent( + `<!-- wp:non-existing-block-here --><img src onerror=alert(1)>` + ); // Give the browser time to show the alert. await page.evaluate( () => new Promise( window.requestIdleCallback ) ); @@ -31,6 +29,7 @@ test.describe( 'missing block', () => { } ); test( 'should strip potentially malicious script tags', async ( { + editor, page, } ) => { let hasAlert = false; @@ -39,12 +38,9 @@ test.describe( 'missing block', () => { hasAlert = true; } ); - await page.evaluate( () => { - const block = window.wp.blocks.parse( - `<!-- wp:non-existing-block-here --><script>alert("EVIL");</script>` - ); - window.wp.data.dispatch( 'core/block-editor' ).resetBlocks( block ); - } ); + await editor.setContent( + `<!-- wp:non-existing-block-here --><script>alert("EVIL");</script>` + ); // Give the browser time to show the alert. await page.evaluate( () => new Promise( window.requestIdleCallback ) ); diff --git a/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js b/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js index ac6094c3d3eac3..1d54f25b1a7cc9 100644 --- a/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js +++ b/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js @@ -112,7 +112,11 @@ test.describe( 'Navigation block - Frontend interactivity', () => { // Test: overlay menu focuses on first element after opening await expect( overlayMenuFirstElement ).toBeFocused(); - // Not Tested: overlay menu traps focus + // Test: overlay menu traps focus + await pageUtils.pressKeys( 'Tab', { times: 2, delay: 50 } ); + await expect( closeMenuButton ).toBeFocused(); + await pageUtils.pressKeys( 'Shift+Tab', { times: 2, delay: 50 } ); + await expect( overlayMenuFirstElement ).toBeFocused(); // Test: overlay menu closes on click on close menu button await closeMenuButton.click(); diff --git a/test/e2e/specs/editor/plugins/post-type-locking.spec.js b/test/e2e/specs/editor/plugins/post-type-locking.spec.js index 5c1d6ebf06dac0..cdfce4c08dcc9b 100644 --- a/test/e2e/specs/editor/plugins/post-type-locking.spec.js +++ b/test/e2e/specs/editor/plugins/post-type-locking.spec.js @@ -21,14 +21,6 @@ test.describe( 'Post-type locking', () => { await admin.createNewPost( { postType: 'locked-all-post' } ); } ); - test( 'should disable the inserter', async ( { page } ) => { - await expect( - page - .getByRole( 'toolbar', { name: 'Document tools' } ) - .getByRole( 'button', { name: 'Toggle block inserter' } ) - ).toBeDisabled(); - } ); - test( 'should not allow blocks to be removed', async ( { editor, page, @@ -147,31 +139,6 @@ test.describe( 'Post-type locking', () => { 'The content of your post doesn’t match the template assigned to your post type.' ); } ); - - test( 'should not allow blocks to be inserted in inner blocks', async ( { - editor, - page, - } ) => { - await editor.canvas - .getByRole( 'button', { - name: 'Two columns; equal split', - } ) - .click(); - - await expect( - page - .getByRole( 'document', { - name: 'Block: Column (1 of 2)', - } ) - .getByRole( 'button', { name: 'Add block' } ) - ).toBeHidden(); - - await expect( - page - .getByRole( 'toolbar', { name: 'Document tools' } ) - .getByRole( 'button', { name: 'Toggle block inserter' } ) - ).toBeDisabled(); - } ); } ); test.describe( 'template_lock insert', () => { @@ -179,14 +146,6 @@ test.describe( 'Post-type locking', () => { await admin.createNewPost( { postType: 'locked-insert-post' } ); } ); - test( 'should disable the inserter', async ( { page } ) => { - await expect( - page - .getByRole( 'toolbar', { name: 'Document tools' } ) - .getByRole( 'button', { name: 'Toggle block inserter' } ) - ).toBeDisabled(); - } ); - test( 'should not allow blocks to be removed', async ( { editor, page, diff --git a/test/e2e/specs/editor/various/allowed-patterns.spec.js b/test/e2e/specs/editor/various/allowed-patterns.spec.js new file mode 100644 index 00000000000000..e592f776c61dd8 --- /dev/null +++ b/test/e2e/specs/editor/various/allowed-patterns.spec.js @@ -0,0 +1,86 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Allowed Patterns', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( 'gutenberg-test-allowed-patterns' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-allowed-patterns' + ); + } ); + + test( 'should show all patterns when all blocks are allowed', async ( { + admin, + page, + } ) => { + await admin.createNewPost(); + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + await page + .getByRole( 'region', { + name: 'Block Library', + } ) + .getByRole( 'searchbox', { + name: 'Search for blocks and patterns', + } ) + .fill( 'Test:' ); + + await expect( + page + .getByRole( 'listbox', { name: 'Block patterns' } ) + .getByRole( 'option' ) + ).toHaveText( [ + 'Test: Single heading', + 'Test: Single paragraph', + 'Test: Paragraph inside group', + ] ); + } ); + + test.describe( 'with a small subset of allowed blocks', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'gutenberg-test-allowed-patterns-disable-blocks' + ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-allowed-patterns-disable-blocks' + ); + } ); + + test( 'should show only allowed patterns', async ( { + admin, + page, + } ) => { + await admin.createNewPost(); + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + await page + .getByRole( 'region', { + name: 'Block Library', + } ) + .getByRole( 'searchbox', { + name: 'Search for blocks and patterns', + } ) + .fill( 'Test:' ); + + await expect( + page + .getByRole( 'listbox', { name: 'Block patterns' } ) + .getByRole( 'option' ) + ).toHaveText( [ 'Test: Single heading' ] ); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js b/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js new file mode 100644 index 00000000000000..0e8c5c8e7bf537 --- /dev/null +++ b/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js @@ -0,0 +1,220 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +async function addTestParagraphBlocks( { editor, page } ) { + await test.step( 'add test paragraph blocks', async () => { + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( '1st' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '2nd' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '3rd' ); + } ); +} + +test.describe( 'Block editor keyboard shortcuts', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.describe( 'move blocks - single block selected', () => { + test( 'should move the block up', async ( { + editor, + page, + pageUtils, + } ) => { + await addTestParagraphBlocks( { editor, page } ); + await pageUtils.pressKeys( 'secondary+t', { times: 2 } ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '3rd' }, + }, + { + name: 'core/paragraph', + attributes: { content: '1st' }, + }, + { + name: 'core/paragraph', + attributes: { content: '2nd' }, + }, + ] ); + } ); + + test( 'should move the block down', async ( { + editor, + page, + pageUtils, + } ) => { + await addTestParagraphBlocks( { editor, page } ); + await page.keyboard.press( 'ArrowUp' ); + await pageUtils.pressKeys( 'secondary+y' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1st' }, + }, + { + name: 'core/paragraph', + attributes: { content: '3rd' }, + }, + { + name: 'core/paragraph', + attributes: { content: '2nd' }, + }, + ] ); + } ); + } ); + + test.describe( 'move blocks - multiple blocks selected', () => { + test( 'should move the blocks up', async ( { + editor, + page, + pageUtils, + } ) => { + await addTestParagraphBlocks( { editor, page } ); + await pageUtils.pressKeys( 'shift+ArrowUp' ); + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Multiple blocks selected' } ) + ).toBeVisible(); + await pageUtils.pressKeys( 'secondary+t' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '2nd' }, + }, + { + name: 'core/paragraph', + attributes: { content: '3rd' }, + }, + { + name: 'core/paragraph', + attributes: { content: '1st' }, + }, + ] ); + } ); + + test( 'should move the blocks down', async ( { + editor, + page, + pageUtils, + } ) => { + await addTestParagraphBlocks( { editor, page } ); + await page.keyboard.press( 'ArrowUp' ); + await pageUtils.pressKeys( 'shift+ArrowUp' ); + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Multiple blocks selected' } ) + ).toBeVisible(); + await pageUtils.pressKeys( 'secondary+y' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '3rd' }, + }, + { + name: 'core/paragraph', + attributes: { content: '1st' }, + }, + { + name: 'core/paragraph', + attributes: { content: '2nd' }, + }, + ] ); + } ); + } ); + + test.describe( 'test shortcuts handling through portals in the same tree', () => { + test( 'should propagate properly and duplicate selected blocks', async ( { + editor, + page, + pageUtils, + } ) => { + await addTestParagraphBlocks( { editor, page } ); + // Multiselect via keyboard. + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + + await editor.clickBlockToolbarButton( 'Options' ); + await expect( + page + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: 'Duplicate' } ) + ).toBeVisible(); + + // Duplicate via keyboard. + await pageUtils.pressKeys( 'primaryShift+d' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + }, + { + name: 'core/paragraph', + }, + { + name: 'core/paragraph', + }, + { + name: 'core/paragraph', + }, + { + name: 'core/paragraph', + }, + { + name: 'core/paragraph', + }, + ] ); + } ); + + test( 'should prevent deleting multiple selected blocks from inputs', async ( { + editor, + page, + pageUtils, + } ) => { + await addTestParagraphBlocks( { editor, page } ); + // Multiselect via keyboard. + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + + await editor.clickBlockToolbarButton( 'Options' ); + await page + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: 'Create pattern' } ) + .click(); + await page + .getByRole( 'dialog', { name: 'Create pattern' } ) + .getByRole( 'textbox', { name: 'Name' } ) + .fill( 'hi' ); + + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'Delete' ); + await page.keyboard.press( 'Escape' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1st' }, + }, + { + name: 'core/paragraph', + attributes: { content: '2nd' }, + }, + { + name: 'core/paragraph', + attributes: { content: '3rd' }, + }, + ] ); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js index b40e7a4b7448a8..eafb468902ef92 100644 --- a/test/e2e/specs/editor/various/block-locking.spec.js +++ b/test/e2e/specs/editor/various/block-locking.spec.js @@ -82,6 +82,12 @@ test.describe( 'Block Locking', () => { await page.click( 'role=checkbox[name="Lock all"]' ); await page.click( 'role=button[name="Apply"]' ); + await expect( + page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Lock' } ) + ).toBeFocused(); + expect( await editor.getEditedPostContent() ) .toBe( `<!-- wp:paragraph {"lock":{"move":false,"remove":false}} --> <p>Some paragraph</p> diff --git a/test/e2e/specs/editor/various/block-renaming.spec.js b/test/e2e/specs/editor/various/block-renaming.spec.js index 19a2818fd3a411..a106d794e2e60b 100644 --- a/test/e2e/specs/editor/various/block-renaming.spec.js +++ b/test/e2e/specs/editor/various/block-renaming.spec.js @@ -37,7 +37,7 @@ test.describe( 'Block Renaming', () => { pageUtils, } ) => { // Turn on block list view by default. - await editor.setPreferences( 'core/edit-site', { + await editor.setPreferences( 'core', { showListViewByDefault: true, } ); diff --git a/packages/e2e-tests/specs/editor/various/core-settings.test.js b/test/e2e/specs/editor/various/core-settings.spec.js similarity index 58% rename from packages/e2e-tests/specs/editor/various/core-settings.test.js rename to test/e2e/specs/editor/various/core-settings.spec.js index 0eb98a2de050b4..9dddc273e6b16e 100644 --- a/packages/e2e-tests/specs/editor/various/core-settings.test.js +++ b/test/e2e/specs/editor/various/core-settings.spec.js @@ -1,10 +1,10 @@ /** * WordPress dependencies */ -import { visitAdminPage } from '@wordpress/e2e-test-utils'; +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); -async function getOptionsValues( selector ) { - await visitAdminPage( 'options.php' ); +async function getOptionsValues( selector, admin, page ) { + await admin.visitAdminPage( 'options.php' ); return page.evaluate( ( theSelector ) => { const inputs = Array.from( document.querySelectorAll( theSelector ) ); return inputs.reduce( ( memo, input ) => { @@ -16,22 +16,32 @@ async function getOptionsValues( selector ) { // It might make sense to include a similar test in WP core (or move this one over). // See discussion here: https://github.com/WordPress/gutenberg/pull/32797#issuecomment-864192088. -describe( 'Settings', () => { - test( 'Regression: updating a specific option will only change its value and will not corrupt others', async () => { +test.describe( 'Settings', () => { + test( 'Regression: updating a specific option will only change its value and will not corrupt others', async ( { + page, + admin, + } ) => { // We won't select the option that we updated and will also remove some // _transient options that seem to change at every update. const optionsInputsSelector = 'form#all-options table.form-table input:not([id*="_transient"]):not([id="blogdescription"])'; - const optionsBefore = await getOptionsValues( optionsInputsSelector ); - - await visitAdminPage( 'options-general.php' ); - await page.type( - 'input#blogdescription', - 'Just another Gutenberg site' + const optionsBefore = await getOptionsValues( + optionsInputsSelector, + admin, + page ); - await page.click( 'input#submit' ); - const optionsAfter = await getOptionsValues( optionsInputsSelector ); + await admin.visitAdminPage( 'options-general.php' ); + await page + .getByRole( 'textbox', { name: 'Tagline' } ) + .fill( 'Just another Gutenberg site' ); + await page.getByRole( 'button', { name: 'Save Changes' } ).click(); + + const optionsAfter = await getOptionsValues( + optionsInputsSelector, + admin, + page + ); Object.entries( optionsBefore ).forEach( ( optionBefore ) => { const [ id ] = optionBefore; diff --git a/test/e2e/specs/editor/various/datepicker.spec.js b/test/e2e/specs/editor/various/datepicker.spec.js new file mode 100644 index 00000000000000..00030efa1fe274 --- /dev/null +++ b/test/e2e/specs/editor/various/datepicker.spec.js @@ -0,0 +1,114 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +// Set browser to a timezone that's different to `timezone`. +test.use( { + timezoneId: 'America/New_York', +} ); + +// The `timezone` setting exposed via REST API only accepts `UTC` +// and timezone strings by location. +const TIMEZONES = [ 'Pacific/Honolulu', 'UTC', 'Australia/Sydney' ]; + +TIMEZONES.forEach( ( timezone ) => { + test.describe( `Datepicker: ${ timezone }`, () => { + let orignalTimezone; + test.beforeAll( async ( { requestUtils } ) => { + orignalTimezone = ( await requestUtils.getSiteSettings() ).timezone; + await requestUtils.updateSiteSettings( { timezone } ); + } ); + + test.beforeEach( async ( { admin, editor } ) => { + await admin.createNewPost(); + await editor.openDocumentSettingsSidebar(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.updateSiteSettings( { + timezone: orignalTimezone, + } ); + } ); + + test( 'should show the publishing date as "Immediately" if the date is not altered', async ( { + page, + } ) => { + await expect( + page.getByRole( 'button', { name: 'Change date' } ) + ).toHaveText( 'Immediately' ); + } ); + + test( 'should show the publishing date if the date is in the past', async ( { + page, + } ) => { + const datepicker = page.getByRole( 'button', { + name: 'Change date', + } ); + await datepicker.click(); + + // Change the publishing date to a year in the future. + await page + .getByRole( 'group', { name: 'Date' } ) + .getByRole( 'spinbutton', { name: 'Year' } ) + .click(); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Escape' ); + + // The expected date format will be "Sep 26, 2018 11:52 pm". + await expect( + page.getByRole( 'button', { name: 'Change date' } ) + ).toContainText( /^[A-Za-z]+\s\d{1,2},\s\d{1,4}/ ); + } ); + + test( 'should show the publishing date if the date is in the future', async ( { + page, + } ) => { + const datepicker = page.getByRole( 'button', { + name: 'Change date', + } ); + await datepicker.click(); + + // Change the publishing date to a year in the future. + await page + .getByRole( 'group', { name: 'Date' } ) + .getByRole( 'spinbutton', { name: 'Year' } ) + .click(); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'Escape' ); + + // The expected date format will be "Sep 26, 2018 11:52 pm". + await expect( + page.getByRole( 'button', { name: 'Change date' } ) + ).toContainText( /^[A-Za-z]+\s\d{1,2},\s\d{1,4}/ ); + } ); + + test( 'should show the publishing date as "Immediately" if the date is cleared', async ( { + page, + } ) => { + const datepicker = page.getByRole( 'button', { + name: 'Change date', + } ); + await datepicker.click(); + + // Change the publishing date to a year in the future. + await page + .getByRole( 'group', { name: 'Date' } ) + .getByRole( 'spinbutton', { name: 'Year' } ) + .click(); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'Escape' ); + + // Clear the date. + await datepicker.click(); + await page + .getByLabel( 'Change publish date' ) + .getByRole( 'button', { name: 'Now' } ) + .click(); + + await expect( + page.getByRole( 'button', { name: 'Change date' } ) + ).toHaveText( 'Immediately' ); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/dropdown-menu.spec.js b/test/e2e/specs/editor/various/dropdown-menu.spec.js new file mode 100644 index 00000000000000..916ef3447d80a4 --- /dev/null +++ b/test/e2e/specs/editor/various/dropdown-menu.spec.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Dropdown Menu', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'keyboard navigiation', async ( { page, pageUtils } ) => { + await page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + const menuItems = page.locator( + '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]' + ); + const totalItems = await menuItems.count(); + + // Catch any issues with the selector, which could cause a false positive test result. + expect( totalItems ).toBeGreaterThan( 0 ); + + await test.step( 'allows navigation through each item using arrow keys', async () => { + // Expect the first menu item to be focused. + await expect( menuItems.first() ).toBeFocused(); + + // Arrow down to the last item. + await pageUtils.pressKeys( 'ArrowDown', { times: totalItems - 1 } ); + await expect( menuItems.last() ).toBeFocused(); + + // Arrow back up to the first item. + await pageUtils.pressKeys( 'ArrowUp', { times: totalItems - 1 } ); + await expect( menuItems.first() ).toBeFocused(); + } ); + + await test.step( 'loops to the beginning and end when navigating past the boundaries of the menu', async () => { + // Expect the first menu item to be focused. + await expect( menuItems.first() ).toBeFocused(); + + // Arrow up to the last item. + await page.keyboard.press( 'ArrowUp' ); + await expect( menuItems.last() ).toBeFocused(); + + // Arrow back down to the first item. + await page.keyboard.press( 'ArrowDown' ); + await expect( menuItems.first() ).toBeFocused(); + } ); + + await test.step( 'ignores arrow key navigation that is orthogonal to the orientation of the menu, but stays open', async () => { + // Expect the first menu item to be focused. + await expect( menuItems.first() ).toBeFocused(); + + // Press left and right keys an arbitrary (but > 1) number of times. + await pageUtils.pressKeys( 'ArrowLeft', { times: 5 } ); + await pageUtils.pressKeys( 'ArrowRight', { times: 5 } ); + + // Expect the first menu item to still be focused. + await expect( menuItems.first() ).toBeFocused(); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/editor-modes.spec.js b/test/e2e/specs/editor/various/editor-modes.spec.js new file mode 100644 index 00000000000000..5d5ae9a70ab5cf --- /dev/null +++ b/test/e2e/specs/editor/various/editor-modes.spec.js @@ -0,0 +1,158 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Editing modes (visual/HTML)', () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.createNewPost(); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Hello world!' }, + } ); + } ); + + test( 'should switch between visual and HTML modes', async ( { + editor, + } ) => { + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + + // This block should be in "visual" mode by default. + await expect( paragraphBlock ).toHaveClass( /rich-text/ ); + + // Change editing mode from "Visual" to "HTML". + await editor.clickBlockOptionsMenuItem( 'Edit as HTML' ); + + // Wait for the block to be converted to HTML editing mode. + await expect( paragraphBlock.getByRole( 'textbox' ) ).toBeVisible(); + + // Change editing mode from "HTML" back to "Visual". + await editor.clickBlockOptionsMenuItem( 'Edit visually' ); + + // This block should be in "visual" mode again. + await expect( paragraphBlock ).toHaveClass( /rich-text/ ); + } ); + + test( 'should display sidebar in HTML mode', async ( { editor, page } ) => { + await editor.clickBlockOptionsMenuItem( 'Edit as HTML' ); + await editor.openDocumentSettingsSidebar(); + + // The `drop cap` toggle for the paragraph block should appear, even in + // HTML editing mode. + await page + .getByRole( 'button', { name: 'Typography options' } ) + .click(); + await page + .getByRole( 'menuitemcheckbox', { name: 'Show Drop cap' } ) + .click(); + + await expect( + page.getByRole( 'checkbox', { name: 'Drop cap' } ) + ).toBeVisible(); + } ); + + test( 'should update HTML in HTML mode when sidebar is used', async ( { + editor, + page, + } ) => { + await editor.clickBlockOptionsMenuItem( 'Edit as HTML' ); + await editor.openDocumentSettingsSidebar(); + + const paragraphHTML = editor.canvas + .getByRole( 'document', { + name: 'Block: Paragraph', + } ) + .getByRole( 'textbox' ); + + // Make sure the paragraph content is rendered as expected. + await expect( paragraphHTML ).toHaveValue( '<p>Hello world!</p>' ); + + // Change the `drop cap` using the sidebar. + await page + .getByRole( 'button', { name: 'Typography options' } ) + .click(); + await page + .getByRole( 'menuitemcheckbox', { name: 'Show Drop cap' } ) + .click(); + await page.getByRole( 'checkbox', { name: 'Drop cap' } ).check(); + + // Make sure the HTML content updated. + await expect( paragraphHTML ).toHaveValue( + '<p class="has-drop-cap">Hello world!</p>' + ); + } ); + + test( 'the code editor should unselect blocks and disable the inserter', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.openDocumentSettingsSidebar(); + const editorSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + const activeTab = editorSettings.getByRole( 'tab', { selected: true } ); + + // The Block inspector should be active. + await expect( activeTab ).toHaveText( 'Block' ); + await expect( + editorSettings.locator( '.block-editor-block-card__title' ) + ).toHaveText( 'Paragraph' ); + + // Open the code editor. + await pageUtils.pressKeys( 'secondary+M' ); + + // The Block inspector should not be active anymore. + await expect( activeTab ).not.toHaveText( 'Block' ); + + await editorSettings.getByRole( 'tab', { name: 'Block' } ).click(); + await expect( + editorSettings.locator( '.block-editor-block-inspector__no-blocks' ) + ).toHaveText( 'No block selected.' ); + + await expect( + page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + ).toBeDisabled(); + + // Go back to the visual editor. + await pageUtils.pressKeys( 'secondary+M' ); + } ); + + // Test for regressions of https://github.com/WordPress/gutenberg/issues/24054. + test( 'saves content when using the shortcut in the Code Editor', async ( { + editor, + page, + pageUtils, + } ) => { + // Open the code editor. + await pageUtils.pressKeys( 'secondary+M' ); + + // Change content. + await page.getByRole( 'textbox', { name: 'Type text or HTML' } ) + .fill( `<!-- wp:paragraph --> +<p>Hi world!</p> +<!-- /wp:paragraph -->` ); + + // Save the post using the shortcut. + await pageUtils.pressKeys( 'primary+s' ); + await expect( + page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ) + ).toBeVisible(); + + // Go back to the visual editor. + await pageUtils.pressKeys( 'secondary+M' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'Hi world!' }, + }, + ] ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/invalid-block.spec.js b/test/e2e/specs/editor/various/invalid-block.spec.js new file mode 100644 index 00000000000000..07c04a5a55457e --- /dev/null +++ b/test/e2e/specs/editor/various/invalid-block.spec.js @@ -0,0 +1,119 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Invalid blocks', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should show an invalid block message with clickable options', async ( { + editor, + page, + } ) => { + // Create an empty paragraph with the focus in the block. + await editor.canvas + .getByRole( 'button', { name: 'Add default block' } ) + .click(); + await page.keyboard.type( 'hello' ); + + // Change to HTML mode and close the options. + await editor.clickBlockOptionsMenuItem( 'Edit as HTML' ); + + // Focus on the textarea and enter an invalid paragraph. + await editor.canvas + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .getByRole( 'textbox' ) + .fill( '<p>invalid paragraph' ); + + // Takes the focus away from the block so the invalid warning is triggered. + await editor.saveDraft(); + + // Click on the 'three-dots' menu toggle. + await editor.canvas + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .getByRole( 'button', { name: 'More options' } ) + .click(); + + await page + .getByRole( 'menu', { name: 'More options' } ) + .getByRole( 'menuitem', { name: 'Resolve' } ) + .click(); + + // Check we get the resolve modal with the appropriate contents. + await expect( + page + .getByRole( 'dialog', { name: 'Resolve Block' } ) + .locator( '.block-editor-block-compare__html' ) + ).toHaveText( [ '<p>invalid paragraph', '<p>invalid paragraph</p>' ] ); + } ); + + test( 'should strip potentially malicious on* attributes', async ( { + editor, + page, + } ) => { + let hasAlert = false; + let error = ''; + let warning = ''; + + page.on( 'dialog', () => { + hasAlert = true; + } ); + + page.on( 'console', ( msg ) => { + if ( msg.type() === 'error' ) { + error = msg.text(); + } + + if ( msg.type() === 'warning' ) { + warning = msg.text(); + } + } ); + + await editor.setContent( ` + <!-- wp:paragraph --> + <p>aaaa <img src onerror=alert(1)></x dde></x>1 + <!-- /wp:paragraph --> + ` ); + + // Give the browser time to show the alert. + await expect( + editor.canvas + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .getByRole( 'button', { name: 'Attempt Block Recovery' } ) + ).toBeVisible(); + + expect( hasAlert ).toBe( false ); + expect( error ).toContain( + 'Block validation: Block validation failed' + ); + expect( warning ).toContain( + 'Block validation: Malformed HTML detected' + ); + } ); + + test( 'should not trigger malicious script tags when using a shortcode block', async ( { + editor, + page, + } ) => { + let hasAlert = false; + + page.on( 'dialog', () => { + hasAlert = true; + } ); + + await editor.setContent( ` + <!-- wp:shortcode --> + <animate onbegin=alert(1) attributeName=x dur=1s><script>alert("EVIL");</script><style>@keyframes x{}</style><a style="animation-name:x" onanimationstart="alert(2)"></a> + <!-- /wp:shortcode --> + ` ); + + // Give the browser time to show the alert. + await expect( + editor.canvas.getByRole( 'document', { name: 'Block: Shortcode' } ) + ).toBeVisible(); + + expect( hasAlert ).toBe( false ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/nux.spec.js b/test/e2e/specs/editor/various/nux.spec.js new file mode 100644 index 00000000000000..ff55dbfa54e478 --- /dev/null +++ b/test/e2e/specs/editor/various/nux.spec.js @@ -0,0 +1,138 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'New User Experience (NUX)', () => { + test( 'should show the guide to first-time users', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost( { showWelcomeGuide: true } ); + + const welcomeGuide = page.getByRole( 'dialog', { + name: 'Welcome to the block editor', + } ); + const guideHeading = welcomeGuide.getByRole( 'heading', { level: 1 } ); + const nextButton = welcomeGuide.getByRole( 'button', { name: 'Next' } ); + const prevButton = welcomeGuide.getByRole( 'button', { + name: 'Previous', + } ); + + await expect( guideHeading ).toHaveText( + 'Welcome to the block editor' + ); + + await nextButton.click(); + await expect( guideHeading ).toHaveText( 'Make each block your own' ); + + await prevButton.click(); + // Guide should be on page 1 of 4 + await expect( guideHeading ).toHaveText( + 'Welcome to the block editor' + ); + + // Press the button for Page 2. + await welcomeGuide + .getByRole( 'button', { name: 'Page 2 of 4' } ) + .click(); + await expect( guideHeading ).toHaveText( 'Make each block your own' ); + + // Press the right arrow key for Page 3. + await page.keyboard.press( 'ArrowRight' ); + await expect( guideHeading ).toHaveText( + 'Get to know the block library' + ); + + // Press the right arrow key for Page 4. + await page.keyboard.press( 'ArrowRight' ); + await expect( guideHeading ).toHaveText( + 'Learn how to use the block editor' + ); + + // Click on the *visible* 'Get started' button. + await welcomeGuide + .getByRole( 'button', { name: 'Get started' } ) + .click(); + + // Guide should be closed. + await expect( welcomeGuide ).toBeHidden(); + + // Reload the editor. + await page.reload(); + + // Guide should be closed. + await expect( + editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) + ).toBeVisible(); + await expect( welcomeGuide ).toBeHidden(); + } ); + + test( 'should not show the welcome guide again if it is dismissed', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost( { showWelcomeGuide: true } ); + + const welcomeGuide = page.getByRole( 'dialog', { + name: 'Welcome to the block editor', + } ); + + await expect( welcomeGuide ).toBeVisible(); + await welcomeGuide.getByRole( 'button', { name: 'Close' } ).click(); + + // Reload the editor. + await page.reload(); + await expect( + editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) + ).toBeFocused(); + + await expect( welcomeGuide ).toBeHidden(); + } ); + + test( 'should focus post title field after welcome guide is dismissed and post is empty', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost( { showWelcomeGuide: true } ); + + const welcomeGuide = page.getByRole( 'dialog', { + name: 'Welcome to the block editor', + } ); + + await expect( welcomeGuide ).toBeVisible(); + await welcomeGuide.getByRole( 'button', { name: 'Close' } ).click(); + + await expect( + editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) + ).toBeFocused(); + } ); + + test( 'should show the welcome guide if it is manually opened', async ( { + admin, + page, + } ) => { + await admin.createNewPost(); + const welcomeGuide = page.getByRole( 'dialog', { + name: 'Welcome to the block editor', + } ); + + await expect( welcomeGuide ).toBeHidden(); + + // Manually open the guide + await page + .getByRole( 'region', { + name: 'Editor top bar', + } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + await page + .getByRole( 'menuitemcheckbox', { name: 'Welcome Guide' } ) + .click(); + + await expect( welcomeGuide ).toBeVisible(); + } ); +} ); diff --git a/test/e2e/specs/editor/various/pref-modal.spec.js b/test/e2e/specs/editor/various/pref-modal.spec.js index f99c7d32a22a94..08c3cd322586a1 100644 --- a/test/e2e/specs/editor/various/pref-modal.spec.js +++ b/test/e2e/specs/editor/various/pref-modal.spec.js @@ -9,7 +9,7 @@ test.describe( 'Preferences modal', () => { } ); test.describe( 'Preferences modal adaps to viewport', () => { - test( 'Enable pre-publish flow is visible on desktop ', async ( { + test( 'Enable pre-publish checks is visible on desktop ', async ( { page, } ) => { await page.click( @@ -18,14 +18,14 @@ test.describe( 'Preferences modal', () => { await page.click( 'role=menuitem[name="Preferences"i]' ); const prePublishToggle = page.locator( - 'role=checkbox[name="Enable pre-publish flow"i]' + 'role=checkbox[name="Enable pre-publish checks"i]' ); await expect( prePublishToggle ).toBeVisible(); } ); } ); test.describe( 'Preferences modal adaps to viewport', () => { - test( 'Enable pre-publish flow is not visible on mobile ', async ( { + test( 'Enable pre-publish checks is not visible on mobile ', async ( { page, } ) => { await page.setViewportSize( { width: 500, height: 800 } ); @@ -44,7 +44,7 @@ test.describe( 'Preferences modal', () => { ); const prePublishToggle = page.locator( - 'role=checkbox[name="Enable pre-publish flow"i]' + 'role=checkbox[name="Enable pre-publish checks"i]' ); await expect( generalButton ).toBeVisible(); diff --git a/test/e2e/specs/editor/various/preferences.spec.js b/test/e2e/specs/editor/various/preferences.spec.js new file mode 100644 index 00000000000000..89df0ae7ed9f77 --- /dev/null +++ b/test/e2e/specs/editor/various/preferences.spec.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Preferences', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'remembers sidebar dismissal between sessions', async ( { + editor, + page, + } ) => { + await editor.openDocumentSettingsSidebar(); + + const editorSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + const activeTab = editorSettings.getByRole( 'tab', { selected: true } ); + + // Open by default. + await expect( activeTab ).toHaveText( 'Post' ); + + // Change to "Block" tab. + await editorSettings.getByRole( 'tab', { name: 'Block' } ).click(); + await expect( activeTab ).toHaveText( 'Block' ); + + /** + * Regression test: Reload resets to document tab. + * + * See: https://github.com/WordPress/gutenberg/issues/6377 + * See: https://github.com/WordPress/gutenberg/pull/8995 + */ + await page.reload(); + await expect( activeTab ).toHaveText( 'Post' ); + + // Dismiss. + await editorSettings + .getByRole( 'button', { + name: 'Close Settings', + } ) + .click(); + await expect( activeTab ).toBeHidden(); + + // Remember after reload. + await page.reload(); + await expect( activeTab ).toBeHidden(); + } ); +} ); diff --git a/test/e2e/specs/editor/various/publish-button.spec.js b/test/e2e/specs/editor/various/publish-button.spec.js index b1e4b07a28580f..631ddcd0fe61ba 100644 --- a/test/e2e/specs/editor/various/publish-button.spec.js +++ b/test/e2e/specs/editor/various/publish-button.spec.js @@ -42,18 +42,20 @@ test.describe( 'Post publish button', () => { topBar.getByRole( 'button', { name: 'Publish' } ) ).toBeEnabled(); - const postId = new URL( page.url() ).searchParams.get( 'post' ); const deferred = defer(); await page.route( ( url ) => - url.searchParams.has( - 'rest_route', - encodeURIComponent( `/wp/v2/posts/${ postId }` ) + url.href.includes( + `rest_route=${ encodeURIComponent( '/wp/v2/posts/' ) }` ), - async ( route ) => { - await deferred; - await route.continue(); + async ( route, request ) => { + if ( request.method() === 'POST' ) { + await deferred; + await route.continue(); + } else { + await route.continue(); + } } ); diff --git a/test/e2e/specs/editor/various/publish-panel.spec.js b/test/e2e/specs/editor/various/publish-panel.spec.js index 1ea72d7eb11a22..4d3ae774dfc11a 100644 --- a/test/e2e/specs/editor/various/publish-panel.spec.js +++ b/test/e2e/specs/editor/various/publish-panel.spec.js @@ -57,4 +57,60 @@ test.describe( 'Post publish panel', () => { ) ).toBeFocused(); } ); + + test( 'should move focus to the publish button in the panel', async ( { + editor, + page, + } ) => { + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Test Post' ); + await page + .getByRole( 'region', 'Editor top bar' ) + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + + await expect( + page + .getByRole( 'region', { name: 'Editor publish' } ) + .locator( ':focus' ) + ).toHaveText( 'Publish' ); + } ); + + test( 'should focus on the post list after publishing', async ( { + editor, + page, + } ) => { + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Test Post' ); + await editor.publishPost(); + + await expect( + page + .getByRole( 'region', { name: 'Editor publish' } ) + .locator( ':focus' ) + ).toHaveText( 'Test Post' ); + } ); + + test( 'should retain focus within the panel', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Test Post' ); + await page + .getByRole( 'region', 'Editor top bar' ) + .getByRole( 'button', { name: 'Publish', exact: true } ) + .click(); + await pageUtils.pressKeys( 'shift+Tab' ); + + await expect( + page.getByRole( 'checkbox', { + name: 'Always show pre-publish checks.', + } ) + ).toBeFocused(); + } ); } ); diff --git a/test/e2e/specs/editor/various/publishing.spec.js b/test/e2e/specs/editor/various/publishing.spec.js new file mode 100644 index 00000000000000..8f448c58e58bd4 --- /dev/null +++ b/test/e2e/specs/editor/various/publishing.spec.js @@ -0,0 +1,164 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +const POST_TYPES = [ 'post', 'page' ]; + +test.describe( 'Publishing', () => { + POST_TYPES.forEach( ( postType ) => { + test.describe( `${ postType } locking prevent saving`, () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost( { postType } ); + } ); + + test( `disables the publish button when a ${ postType } is locked`, async ( { + editor, + page, + } ) => { + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'E2E Test Post' ); + + await page.evaluate( () => + window.wp.data + .dispatch( 'core/editor' ) + .lockPostSaving( 'futurelock' ) + ); + + // Open publish panel. + await page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Publish' } ) + .click(); + + // Publish button should be disabled. + await expect( + page + .getByRole( 'region', { name: 'Editor publish' } ) + .getByRole( 'button', { name: 'Publish', exact: true } ) + ).toBeDisabled(); + } ); + + test( `disables the save shortcut when a ${ postType } is locked`, async ( { + editor, + page, + pageUtils, + } ) => { + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'E2E Test Post' ); + + await page.evaluate( () => + window.wp.data + .dispatch( 'core/editor' ) + .lockPostSaving( 'futurelock' ) + ); + + await pageUtils.pressKeys( 'primary+s' ); + + await expect( + page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Save draft' } ) + ).toBeEnabled(); + } ); + } ); + } ); + + POST_TYPES.forEach( ( postType ) => { + test.describe( `a ${ postType } with pre-publish checks disabled`, () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.createNewPost( { postType } ); + await editor.setPreferences( 'core/edit-post', { + isPublishSidebarEnabled: false, + } ); + } ); + + test.afterEach( async ( { editor } ) => { + await editor.setPreferences( 'core/edit-post', { + isPublishSidebarEnabled: true, + } ); + } ); + + test( `should publish the ${ postType } without opening the post-publish sidebar`, async ( { + editor, + page, + } ) => { + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'E2E Test Post' ); + + // Publish the post. + await page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Publish' } ) + .click(); + + const publishPanel = page.getByRole( 'region', { + name: 'Editor publish', + } ); + + // The pre-publishing panel should have been not shown. + await expect( + publishPanel.getByRole( 'button', { + name: 'Publish', + exact: true, + } ) + ).toBeHidden(); + + // The post-publishing panel should have been not shown. + await expect( + publishPanel.getByRole( 'button', { + name: 'View Post', + } ) + ).toBeHidden(); + + await expect( + page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'published' } ) + ).toBeVisible(); + } ); + } ); + } ); + + POST_TYPES.forEach( ( postType ) => { + test.describe( `a ${ postType } in small viewports`, () => { + test.beforeEach( async ( { admin, editor, pageUtils } ) => { + await admin.createNewPost( { postType } ); + await editor.setPreferences( 'core/edit-post', { + isPublishSidebarEnabled: false, + } ); + await pageUtils.setBrowserViewport( 'small' ); + } ); + + test.afterEach( async ( { editor, pageUtils } ) => { + await editor.setPreferences( 'core/edit-post', { + isPublishSidebarEnabled: true, + } ); + await pageUtils.setBrowserViewport( 'large' ); + } ); + + test( 'should ignore the pre-publish checks and show the publish panel', async ( { + editor, + page, + } ) => { + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'E2E Test Post' ); + + await page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Publish' } ) + .click(); + + await expect( + page + .getByRole( 'region', { name: 'Editor publish' } ) + .getByRole( 'button', { name: 'Publish', exact: true } ) + ).toBeVisible(); + } ); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/scheduling.spec.js b/test/e2e/specs/editor/various/scheduling.spec.js new file mode 100644 index 00000000000000..1fa41a79ea7ccc --- /dev/null +++ b/test/e2e/specs/editor/various/scheduling.spec.js @@ -0,0 +1,90 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +// The `timezone` setting exposed via REST API only accepts `UTC` +// and timezone strings by location. +const TIMEZONES = [ 'Pacific/Honolulu', 'UTC', 'Australia/Sydney' ]; + +test.describe( 'Scheduling', () => { + TIMEZONES.forEach( ( timezone ) => { + test.describe( `Timezone ${ timezone }`, () => { + let orignalTimezone; + test.beforeAll( async ( { requestUtils } ) => { + orignalTimezone = ( await requestUtils.getSiteSettings() ) + .timezone; + + await requestUtils.updateSiteSettings( { timezone } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.updateSiteSettings( { + timezone: orignalTimezone, + } ); + } ); + + test( 'Should change publishing button text from "Publish" to "Schedule"', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await editor.openDocumentSettingsSidebar(); + + const topBar = page.getByRole( 'region', { + name: 'Editor top bar', + } ); + + await expect( + topBar.getByRole( 'button', { name: 'Publish' } ) + ).toBeVisible(); + + // Open the datepicker. + await page + .getByRole( 'button', { name: 'Change date' } ) + .click(); + + // Change the publishing date to a year in the future. + await page + .getByRole( 'group', { name: 'Date' } ) + .getByRole( 'spinbutton', { name: 'Year' } ) + .click(); + await page.keyboard.press( 'ArrowUp' ); + + // Close the datepicker. + await page.keyboard.press( 'Escape' ); + + await expect( + topBar.getByRole( 'button', { name: 'Schedule…' } ) + ).toBeVisible(); + } ); + } ); + } ); + + test( 'should keep date time UI focused when the previous and next month buttons are clicked', async ( { + admin, + editor, + page, + } ) => { + await admin.createNewPost(); + await editor.openDocumentSettingsSidebar(); + await page.getByRole( 'button', { name: 'Change date' } ).click(); + + const calendar = page.getByRole( 'application', { name: 'Calendar' } ); + const prevMonth = calendar.getByRole( 'button', { + name: 'View previous month', + } ); + const nextMonth = calendar.getByRole( 'button', { + name: 'View next month', + } ); + + await prevMonth.click(); + await expect( prevMonth ).toBeFocused(); + await expect( calendar ).toBeVisible(); + + await nextMonth.click(); + await expect( nextMonth ).toBeFocused(); + await expect( calendar ).toBeVisible(); + } ); +} ); diff --git a/test/e2e/specs/editor/various/sidebar.spec.js b/test/e2e/specs/editor/various/sidebar.spec.js new file mode 100644 index 00000000000000..c1c9136a9afda2 --- /dev/null +++ b/test/e2e/specs/editor/various/sidebar.spec.js @@ -0,0 +1,136 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Sidebar', () => { + test.beforeAll( async ( { requestUtils } ) => { + // The test expects clean user preferences. + await requestUtils.resetPreferences(); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should have sidebar visible at the start with document sidebar active on desktop', async ( { + page, + pageUtils, + } ) => { + await pageUtils.setBrowserViewport( 'large' ); + + const activeTab = page + .getByRole( 'region', { + name: 'Editor settings', + } ) + .getByRole( 'tab', { selected: true } ); + + await expect( activeTab ).toBeVisible(); + await expect( activeTab ).toHaveText( 'Post' ); + } ); + + test( 'should have the sidebar closed by default on mobile', async ( { + page, + pageUtils, + } ) => { + await pageUtils.setBrowserViewport( 'small' ); + + await expect( + page.getByRole( 'region', { + name: 'Editor settings', + } ) + ).toBeHidden(); + } ); + + test( 'should close the sidebar when resizing from desktop to mobile', async ( { + page, + pageUtils, + } ) => { + await pageUtils.setBrowserViewport( 'large' ); + const settingsSideber = page.getByRole( 'region', { + name: 'Editor settings', + } ); + + await expect( settingsSideber ).toBeVisible(); + + await pageUtils.setBrowserViewport( 'small' ); + + // Sidebar should be closed when resizing to mobile. + await expect( settingsSideber ).toBeHidden(); + } ); + + test( 'should reopen sidebar the sidebar when resizing from mobile to desktop if the sidebar was closed automatically', async ( { + page, + pageUtils, + } ) => { + await pageUtils.setBrowserViewport( 'large' ); + await pageUtils.setBrowserViewport( 'small' ); + const settingsSideber = page.getByRole( 'region', { + name: 'Editor settings', + } ); + + await expect( settingsSideber ).toBeHidden(); + await pageUtils.setBrowserViewport( 'large' ); + await expect( settingsSideber ).toBeVisible(); + } ); + + test( 'should preserve tab order while changing active tab', async ( { + page, + pageUtils, + } ) => { + // Region navigate to Sidebar. + await pageUtils.pressKeys( 'ctrl+`' ); + + // Tab lands at first (presumed selected) option "Post". + await page.keyboard.press( 'Tab' ); + + const activeTab = page + .getByRole( 'region', { + name: 'Editor settings', + } ) + .getByRole( 'tab', { selected: true } ); + + // The Post tab should be focused and selected. + await expect( activeTab ).toHaveText( 'Post' ); + await expect( activeTab ).toBeFocused(); + + // Arrow key into and activate "Block". + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Space' ); + + // The Block tab should be focused and selected. + await expect( activeTab ).toHaveText( 'Block' ); + await expect( activeTab ).toBeFocused(); + } ); + + test( 'should be possible to programmatically remove Document Settings panels', async ( { + page, + } ) => { + const documentSettingsPanels = page + .getByRole( 'tabpanel', { name: 'Post' } ) + .getByRole( 'heading', { level: 2 } ); + + await expect( documentSettingsPanels ).toHaveText( [ + 'Summary', + 'Categories', + 'Tags', + 'Featured image', + 'Excerpt', + 'Discussion', + ] ); + + await page.evaluate( () => { + const { removeEditorPanel } = + window.wp.data.dispatch( 'core/editor' ); + + removeEditorPanel( 'taxonomy-panel-category' ); + removeEditorPanel( 'taxonomy-panel-post_tag' ); + removeEditorPanel( 'featured-image' ); + removeEditorPanel( 'post-excerpt' ); + removeEditorPanel( 'discussion-panel' ); + removeEditorPanel( 'post-status' ); + } ); + + await expect( documentSettingsPanels ).toHaveCount( 0 ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/taxonomies.spec.js b/test/e2e/specs/editor/various/taxonomies.spec.js new file mode 100644 index 00000000000000..efd8c9c6ee7fe0 --- /dev/null +++ b/test/e2e/specs/editor/various/taxonomies.spec.js @@ -0,0 +1,136 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +function generateRandomNumber() { + return Math.round( 1 + Math.random() * ( Number.MAX_SAFE_INTEGER - 1 ) ); +} + +test.describe( 'Taxonomies', () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.createNewPost(); + await editor.openDocumentSettingsSidebar(); + } ); + + test( 'should be able to open the categories panel and create a new main category', async ( { + editor, + page, + } ) => { + // Open the Document -> Categories panel. + const panelToggle = page.getByRole( 'button', { + name: 'Categories', + } ); + + if ( + ( await panelToggle.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await panelToggle.click(); + } + + await page + .getByRole( 'button', { + name: 'Add New Category', + expanded: false, + } ) + .click(); + await page + .getByRole( 'textbox', { name: 'New Category Name' } ) + .fill( 'z rand category 1' ); + await page.keyboard.press( 'Enter' ); + + const categories = page.getByRole( 'group', { name: 'Categories' } ); + const selectedCategories = categories.getByRole( 'checkbox', { + checked: true, + } ); + const newCategory = categories.getByRole( 'checkbox', { + name: 'z rand category 1', + } ); + + await expect( selectedCategories ).toHaveCount( 1 ); + await expect( newCategory ).toBeChecked(); + + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Hello World' ); + await editor.publishPost(); + await page.reload(); + + // The category selection was persisted after the publish process. + await expect( selectedCategories ).toHaveCount( 1 ); + await expect( newCategory ).toBeChecked(); + } ); + + test( 'should be able to open the tags panel and create a new tag', async ( { + editor, + page, + } ) => { + // Open the Document -> Tags panel. + const panelToggle = page.getByRole( 'button', { + name: 'Tags', + } ); + + if ( + ( await panelToggle.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await panelToggle.click(); + } + + const tagName = 'tag-' + generateRandomNumber(); + const tags = page.locator( '.components-form-token-field__token-text' ); + + await page + .getByRole( 'combobox', { name: 'Add New Tag' } ) + .fill( tagName ); + await page.keyboard.press( 'Enter' ); + + await expect( tags ).toHaveCount( 1 ); + await expect( tags ).toContainText( tagName ); + + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Hello World' ); + await editor.publishPost(); + await page.reload(); + + await expect( tags ).toHaveCount( 1 ); + await expect( tags ).toContainText( tagName ); + } ); + + // See: https://github.com/WordPress/gutenberg/pull/21693. + test( `should be able to create a new tag with ' on the name`, async ( { + editor, + page, + } ) => { + // Open the Document -> Tags panel. + const panelToggle = page.getByRole( 'button', { + name: 'Tags', + } ); + + if ( + ( await panelToggle.getAttribute( 'aria-expanded' ) ) === 'false' + ) { + await panelToggle.click(); + } + + const tagName = "tag'-" + generateRandomNumber(); + const tags = page.locator( '.components-form-token-field__token-text' ); + + await page + .getByRole( 'combobox', { name: 'Add New Tag' } ) + .fill( tagName ); + await page.keyboard.press( 'Enter' ); + + await expect( tags ).toHaveCount( 1 ); + await expect( tags ).toContainText( tagName ); + + await editor.canvas + .getByRole( 'textbox', { name: 'Add title' } ) + .fill( 'Hello World' ); + await editor.publishPost(); + await page.reload(); + + await expect( tags ).toHaveCount( 1 ); + await expect( tags ).toContainText( tagName ); + } ); +} ); diff --git a/test/e2e/specs/site-editor/list-view.spec.js b/test/e2e/specs/site-editor/list-view.spec.js index 74d3559d4e3d27..9b7a1c17a9ce1d 100644 --- a/test/e2e/specs/site-editor/list-view.spec.js +++ b/test/e2e/specs/site-editor/list-view.spec.js @@ -30,7 +30,7 @@ test.describe( 'Site Editor List View', () => { ).toBeHidden(); // Turn on block list view by default. - await editor.setPreferences( 'core/edit-site', { + await editor.setPreferences( 'core', { showListViewByDefault: true, } ); @@ -41,7 +41,7 @@ test.describe( 'Site Editor List View', () => { ).toBeVisible(); // The preferences cleanup. - await editor.setPreferences( 'core/edit-site', { + await editor.setPreferences( 'core', { showListViewByDefault: false, } ); } ); @@ -118,6 +118,7 @@ test.describe( 'Site Editor List View', () => { // out of range of the sidebar region. Must shift+tab 1 time to reach // close button before list view area. await pageUtils.pressKeys( 'shift+Tab' ); + await pageUtils.pressKeys( 'shift+Tab' ); await expect( page .getByRole( 'region', { name: 'List View' } ) diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index 6de94c3c2d6732..a04359730421b5 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -196,15 +196,15 @@ test.describe( 'Pages', () => { await templateOptionsButton.click(); const templatePreviewButton = page .getByRole( 'menu', { name: 'Template options' } ) - .getByRole( 'menuitem', { name: 'Template preview' } ); + .getByRole( 'menuitemcheckbox', { name: 'Template preview' } ); await expect( templatePreviewButton ).toHaveAttribute( - 'aria-pressed', + 'aria-checked', 'true' ); await templatePreviewButton.click(); await expect( templatePreviewButton ).toHaveAttribute( - 'aria-pressed', + 'aria-checked', 'false' ); @@ -229,7 +229,7 @@ test.describe( 'Pages', () => { await templateOptionsButton.click(); await templatePreviewButton.click(); await expect( templatePreviewButton ).toHaveAttribute( - 'aria-pressed', + 'aria-checked', 'true' ); diff --git a/test/e2e/specs/widgets/editing-widgets.spec.js b/test/e2e/specs/widgets/editing-widgets.spec.js new file mode 100644 index 00000000000000..28e9aac437572c --- /dev/null +++ b/test/e2e/specs/widgets/editing-widgets.spec.js @@ -0,0 +1,724 @@ +/** + * WordPress dependencies + */ +const { + test: base, + expect, +} = require( '@wordpress/e2e-test-utils-playwright' ); + +/** @typedef {import('@wordpress/e2e-test-utils-playwright').Editor} Editor */ +/** @typedef {import('@playwright/test').Locator} Locator */ + +/** @type {ReturnType<typeof base.extend<{widgetsScreen: WidgetsScreen}>>} */ +const test = base.extend( { + widgetsScreen: async ( { page, editor }, use ) => { + await use( new WidgetsScreen( { page, editor } ) ); + }, +} ); + +test.describe( 'Widgets screen', () => { + test.beforeEach( async ( { admin, editor } ) => { + await admin.visitAdminPage( 'widgets.php' ); + + await editor.setPreferences( 'core/edit-widgets', { + welcomeGuide: false, + } ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllWidgets(); + } ); + + test.beforeAll( async ( { requestUtils } ) => { + await Promise.all( [ + // TODO: Ideally we can bundle our test theme directly in the repo. + requestUtils.activateTheme( 'twentytwenty' ), + requestUtils.deleteAllWidgets(), + ] ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'Should insert content using the global inserter', async ( { + page, + pageUtils, + widgetsScreen, + } ) => { + await expect( + widgetsScreen.updateButton, + 'Update button should start out disabled' + ).toBeDisabled(); + + const [ firstWidgetArea, secondWidgetArea ] = + await widgetsScreen.widgetAreas.all(); + + await page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + await expect( + blockLibrary.getByRole( 'heading', { level: 2 } ).nth( 0 ), + 'Check that there are categorizations in the inserter (#26329)' + ).toBeVisible(); + + const addParagraphBlock = + await widgetsScreen.getBlockInGlobalInserter( 'Paragraph' ); + await addParagraphBlock.hover(); + // TODO: We can add a test for the insertion indicator here. + await addParagraphBlock.click(); + + await expect( + widgetsScreen.updateButton, + 'Adding content should enable the Update button' + ).toBeEnabled(); + + const addedParagraphBlockInFirstWidgetArea = firstWidgetArea.getByRole( + 'document', + { name: /^Empty block/ } + ); + + await addedParagraphBlockInFirstWidgetArea.focus(); + await page.keyboard.type( 'First Paragraph' ); + + await widgetsScreen.getBlockInGlobalInserter( 'Paragraph' ); + await pageUtils.pressKeys( 'Tab', { times: 2 } ); + // TODO: We can add a test for the insertion indicator here. + await addParagraphBlock.click(); + + await addedParagraphBlockInFirstWidgetArea.focus(); + await page.keyboard.type( 'Second Paragraph' ); + + const addShortCodeBlock = + await widgetsScreen.getBlockInGlobalInserter( 'Shortcode' ); + await addShortCodeBlock.click(); + + const shortCodeInput = page.getByRole( 'textbox', { + name: 'Shortcode text', + } ); + await shortCodeInput.focus(); + // The famous Big Buck Bunny video. + await shortCodeInput.type( + '[video src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]' + ); + + // Add to the second widget area. + await secondWidgetArea.click(); + await widgetsScreen.getBlockInGlobalInserter( 'Paragraph' ); + await addParagraphBlock.click(); + + const addedParagraphBlockInSecondWidgetArea = + secondWidgetArea.getByRole( 'document', { + name: /^Empty block/, + } ); + await addedParagraphBlockInSecondWidgetArea.focus(); + await page.keyboard.type( 'Third Paragraph' ); + + await expect.poll( widgetsScreen.getWidgetAreaBlocks ).toMatchObject( { + 'sidebar-1': [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + { + name: 'core/shortcode', + attributes: { + text: '[video src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"]', + }, + }, + ], + 'sidebar-2': [ + { + name: 'core/paragraph', + attributes: { content: 'Third Paragraph' }, + }, + ], + } ); + } ); + + test( 'Should insert content using the inline inserter', async ( { + page, + widgetsScreen, + } ) => { + const firstWidgetArea = widgetsScreen.widgetAreas.first(); + + const inlineInserterButton = firstWidgetArea.getByRole( 'button', { + name: 'Add block', + } ); + await inlineInserterButton.click(); + + const inlineQuickInserter = page.getByRole( 'listbox', { + name: 'Blocks', + } ); + + const paragraphBlock = inlineQuickInserter.getByRole( 'option', { + name: 'Paragraph', + } ); + await paragraphBlock.click(); + + const firstParagraphBlock = firstWidgetArea.getByRole( 'document', { + name: /^Empty block/, + } ); + await firstParagraphBlock.focus(); + await page.keyboard.type( 'First Paragraph' ); + + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Second Paragraph' ); + + const secondParagraphBlock = firstWidgetArea + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .filter( { hasText: 'Second Paragraph' } ); + + const secondParagraphBlockBoundingBox = + await secondParagraphBlock.boundingBox(); + + // Click outside the block to move the focus back to the widget area. + await firstWidgetArea.click( { + position: { + x: 0, + y: ( await firstWidgetArea.boundingBox() ).height - 1, + }, + } ); + + // Hover above the last block to trigger the inline inserter between blocks. + await page.mouse.move( + secondParagraphBlockBoundingBox.x + + secondParagraphBlockBoundingBox.width / 2, + secondParagraphBlockBoundingBox.y - 10 + ); + + // There will be 2 matches here. + // One is the in-between inserter, + // and the other one is the button block appender. + const inBetweenInserterButton = page + .getByRole( 'button', { + name: 'Add block', + } ) + .first(); + await inBetweenInserterButton.click(); + + const inserterSearchBox = page.getByRole( 'searchbox', { + name: 'Search for blocks and patterns', + } ); + await expect( inserterSearchBox ).toBeFocused(); + + await page.keyboard.type( 'Heading' ); + + await inlineQuickInserter + .getByRole( 'option', { name: 'Heading' } ) + .click(); + + await expect( + firstWidgetArea.getByRole( 'document', { + name: 'Block: Heading', + } ) + ).toBeFocused(); + await page.keyboard.type( 'My Heading' ); + + await widgetsScreen.saveWidgets(); + + await expect.poll( widgetsScreen.getWidgetAreaBlocks ).toMatchObject( { + 'sidebar-1': [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { + name: 'core/heading', + attributes: { content: 'My Heading' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + ], + } ); + } ); + + // This test is broken because the REST API endpoint to delete the marquee doesn't work. + // eslint-disable-next-line playwright/no-skipped-test + test.describe.skip( 'Function widgets', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllWidgets(); + await requestUtils.activatePlugin( + 'gutenberg-test-marquee-widget' + ); + } ); + + test.beforeEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllWidgets(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-marquee-widget' + ); + } ); + + test( 'Should add and save the marquee widget', async ( { + page, + editor, + widgetsScreen, + } ) => { + const firstWidgetArea = widgetsScreen.widgetAreas.first(); + await firstWidgetArea.waitFor(); + { + const [ { clientId: firstWidgetAreaClientId } ] = + await editor.getBlocks( { full: true } ); + + await editor.insertBlock( + { + name: 'core/legacy-widget', + attributes: { id: 'marquee_greeting' }, + }, + { clientId: firstWidgetAreaClientId } + ); + } + + const marqueeInputs = page.getByTestId( 'marquee-greeting' ); + + await marqueeInputs.fill( 'Howdy' ); + + // The first marquee is saved after clicking the form save button. + await page + .getByRole( 'document', { name: 'Block: Legacy Widget' } ) + .getByRole( 'button', { name: 'Save' } ) + .click(); + + await widgetsScreen.saveWidgets(); + + await expect + .poll( widgetsScreen.getWidgetAreaBlocks ) + .toMatchObject( { + 'sidebar-1': [ + { + name: 'core/legacy-widget', + attributes: { + id: 'marquee_greeting', + instance: { raw: { title: 'Howdy' } }, + }, + }, + ], + } ); + + await page.reload(); + + await expect + .poll( widgetsScreen.getWidgetAreaBlocks ) + .toMatchObject( { + 'sidebar-1': [ + { + name: 'core/legacy-widget', + attributes: { + id: 'marquee_greeting', + instance: { raw: { title: 'Howdy' } }, + }, + }, + ], + } ); + + await firstWidgetArea.waitFor(); + { + const [ { clientId: firstWidgetAreaClientId } ] = + await editor.getBlocks( { full: true } ); + + await editor.insertBlock( + { + name: 'core/legacy-widget', + attributes: { id: 'marquee_greeting' }, + }, + { clientId: firstWidgetAreaClientId } + ); + } + + await expect( marqueeInputs ).toHaveCount( 2 ); + await marqueeInputs.first().fill( 'first Howdy' ); + await marqueeInputs.last().fill( 'Second Howdy' ); + + // No marquee should be changed without clicking on their "save" button. + // The second marquee shouldn't be stored as a widget. + // See #32978 for more info. + { + await widgetsScreen.saveWidgets(); + + await page.reload(); + + await expect( marqueeInputs ).toHaveCount( 1 ); + await expect + .poll( widgetsScreen.getWidgetAreaBlocks ) + .toMatchObject( { + 'sidebar-1': [ + { + name: 'core/legacy-widget', + attributes: { + id: 'marquee_greeting', + instance: { raw: { title: 'Howdy' } }, + }, + }, + ], + } ); + } + } ); + } ); + + test( 'Should duplicate the widgets', async ( { + page, + editor, + widgetsScreen, + } ) => { + const firstWidgetArea = widgetsScreen.widgetAreas.first(); + await firstWidgetArea.waitFor(); + const [ { clientId: firstWidgetAreaClientId } ] = + await editor.getBlocks( { full: true } ); + + await editor.insertBlock( + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { clientId: firstWidgetAreaClientId } + ); + + const firstParagraphBlock = firstWidgetArea.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await firstParagraphBlock.focus(); + + await editor.showBlockToolbar(); + await editor.clickBlockToolbarButton( 'Options' ); + await page + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem', { name: 'Duplicate' } ) + .click(); + + const firstWidgetAreaBlocks = await editor.getBlocks( { + clientId: firstWidgetAreaClientId, + full: true, + } ); + expect( firstWidgetAreaBlocks[ 0 ].name ).toBe( + firstWidgetAreaBlocks[ 1 ].name + ); + expect( firstWidgetAreaBlocks[ 0 ].content ).toBe( + firstWidgetAreaBlocks[ 1 ].content + ); + expect( firstWidgetAreaBlocks[ 0 ].clientId ).not.toBe( + firstWidgetAreaBlocks[ 1 ].clientId + ); + } ); + + test( 'Should display legacy widgets', async ( { + page, + requestUtils, + widgetsScreen, + } ) => { + // Get the default empty instance of a legacy search widget. + const { instance: defaultSearchInstance } = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/widget-types/search/encode', + data: { instance: {} }, + } ); + + // Create a search widget in the first sidebar using the default instance. + const widget = await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/widgets', + data: { + id_base: 'search', + sidebar: 'sidebar-1', + instance: defaultSearchInstance, + }, + } ); + // Add it to the first widget area. The above request for some reason isn't enough. + await requestUtils.rest( { + method: 'POST', + path: '/wp/v2/sidebars/sidebar-1', + data: { + widgets: [ widget.id ], + }, + } ); + + await page.reload(); + + const legacyWidgetPreviewFrame = page.frameLocator( + '[title="Legacy Widget Preview"]' + ); + + // Expect to have search input. + await expect( + legacyWidgetPreviewFrame.getByRole( 'searchbox', { + includeHidden: true, + } ) + ).toBeVisible(); + + // Focus the Legacy Widget block. + const legacyWidget = page.getByRole( 'document', { + name: 'Block: Legacy Widget', + } ); + await legacyWidget.focus(); + + // There should be a title at the top of the widget form. + await expect( + legacyWidget.getByRole( 'heading', { level: 3 } ) + ).toHaveText( 'Search' ); + + // Update the widget form. + await legacyWidget + .getByRole( 'textbox', { name: 'Title' } ) + .fill( 'Search Title' ); + + // Switch to Navigation mode. + await page.keyboard.press( 'Escape' ); + + // Expect to have search title. + await expect( + legacyWidgetPreviewFrame.getByRole( 'heading', { + includeHidden: true, + } ) + ).toHaveText( 'Search Title' ); + + await expect.poll( widgetsScreen.getWidgetAreaBlocks ).toMatchObject( { + 'sidebar-1': [ + { + name: 'core/legacy-widget', + attributes: { + idBase: 'search', + instance: { raw: { title: 'Search Title' } }, + }, + }, + ], + } ); + } ); + + test( 'allows widgets to be moved between widget areas using the dropdown in the block toolbar', async ( { + page, + editor, + widgetsScreen, + } ) => { + const firstWidgetArea = widgetsScreen.widgetAreas.first(); + await firstWidgetArea.waitFor(); + const [ { clientId: firstWidgetAreaClientId } ] = + await editor.getBlocks( { full: true } ); + + // Insert a paragraph in the first widget area. + await editor.insertBlock( + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { clientId: firstWidgetAreaClientId } + ); + + const firstParagraphBlock = firstWidgetArea.getByRole( 'document', { + name: 'Block: Paragraph', + } ); + await firstParagraphBlock.focus(); + // Move the block to the second widget area. + await editor.showBlockToolbar(); + await editor.clickBlockToolbarButton( 'Move to widget area' ); + await page + .getByRole( 'menu', { name: 'Move to widget area' } ) + .getByRole( 'menuitemradio', { name: 'Footer #2' } ) + .click(); + + await expect.poll( widgetsScreen.getWidgetAreaBlocks ).toMatchObject( { + 'sidebar-1': [], + 'sidebar-2': [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + ], + } ); + } ); + + test( 'Allows widget deletion to be undone', async ( { + page, + editor, + pageUtils, + widgetsScreen, + } ) => { + const firstWidgetArea = widgetsScreen.widgetAreas.first(); + await firstWidgetArea.waitFor(); + const [ { clientId: firstWidgetAreaClientId } ] = + await editor.getBlocks( { full: true } ); + + await editor.insertBlock( + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { clientId: firstWidgetAreaClientId } + ); + await editor.insertBlock( + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + { clientId: firstWidgetAreaClientId } + ); + + await widgetsScreen.saveWidgets(); + + // Delete the last block and save again. + await firstWidgetArea + .getByRole( 'document', { name: 'Block: Paragraph' } ) + .filter( { hasText: 'Second Paragraph' } ) + .focus(); + await pageUtils.pressKeys( 'access+z' ); + await widgetsScreen.saveWidgets(); + + await expect.poll( widgetsScreen.getWidgetAreaBlocks ).toMatchObject( { + 'sidebar-1': [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + ], + } ); + + // Undo block deletion and save again. + await pageUtils.pressKeys( 'primary+z' ); + await widgetsScreen.saveWidgets(); + + // Reload the page to make sure changes were actually saved. + await page.reload(); + + await expect.poll( widgetsScreen.getWidgetAreaBlocks ).toMatchObject( { + 'sidebar-1': [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + ], + } ); + } ); + + test( 'can toggle sidebar list view', async ( { page } ) => { + const toggleListViewButton = page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'List View' } ); + await toggleListViewButton.click(); + + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', + } ); + await expect( listView ).toBeVisible(); + + await expect( listView.getByRole( 'gridcell' ) ).toHaveCount( 3 ); + + await toggleListViewButton.click(); + await expect( listView ).toBeHidden(); + } ); + + // Check for regressions of https://github.com/WordPress/gutenberg/issues/38002. + test( 'allows blocks to be added on mobile viewports', async ( { + page, + pageUtils, + widgetsScreen, + } ) => { + await pageUtils.setBrowserViewport( 'small' ); + + const firstWidgetArea = widgetsScreen.widgetAreas.first(); + + const addParagraphBlock = + await widgetsScreen.getBlockInGlobalInserter( 'Paragraph' ); + await addParagraphBlock.click(); + + await firstWidgetArea + .getByRole( 'document', { + name: /^Empty block/, + } ) + .focus(); + await page.keyboard.type( 'First Paragraph' ); + + await expect.poll( widgetsScreen.getWidgetAreaBlocks ).toMatchObject( { + 'sidebar-1': [ + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + ], + } ); + } ); +} ); + +class WidgetsScreen { + /** @type {import('@playwright/test').Page} */ + #page; + /** @type {Editor} */ + #editor; + + constructor( { page, editor } ) { + this.#page = page; + this.#editor = editor; + + /** @type {Locator} */ + this.widgetAreas = this.#page.getByRole( 'document', { + name: 'Block: Widget Area', + } ); + /** @type {Locator} */ + this.updateButton = this.#page.getByRole( 'button', { + name: 'Update', + } ); + } + + getWidgetAreaBlocks = async () => { + const widgetAreas = await this.#editor.getBlocks( { full: true } ); + /** @type {Record<string, Awaited<ReturnType<Editor['getBlocks']>>>} */ + const widgetAreaBlocks = {}; + for ( const widgetArea of widgetAreas ) { + const widgetAreaId = widgetArea.attributes.id; + widgetAreaBlocks[ widgetAreaId ] = await this.#editor.getBlocks( { + clientId: widgetArea.clientId, + } ); + } + return widgetAreaBlocks; + }; + + /** + * @param {string} blockName + */ + getBlockInGlobalInserter = async ( blockName ) => { + const blockLibrary = this.#page.getByRole( 'region', { + name: 'Block Library', + } ); + if ( await blockLibrary.isHidden() ) { + await this.#page + .getByRole( 'toolbar', { name: 'Document tools' } ) + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + } + + await blockLibrary + .getByRole( 'searchbox', { + name: 'Search for blocks and patterns', + } ) + .fill( blockName ); + + return blockLibrary.getByRole( 'option', { name: blockName } ); + }; + + saveWidgets = async () => { + await test.step( + 'save widgets', + async () => { + await this.updateButton.click(); + await expect( this.updateButton ).toBeDisabled(); + }, + { box: true } + ); + }; +} diff --git a/test/integration/fixtures/blocks/core__gallery-with-caption.json b/test/integration/fixtures/blocks/core__gallery-with-caption.json index 12b516606641d4..1106c8ccb64b2a 100644 --- a/test/integration/fixtures/blocks/core__gallery-with-caption.json +++ b/test/integration/fixtures/blocks/core__gallery-with-caption.json @@ -8,6 +8,7 @@ "shortCodeTransforms": [], "caption": "Gallery Caption", "imageCrop": true, + "randomOrder": false, "fixedHeight": true, "linkTo": "none", "sizeSlug": "large", diff --git a/test/integration/fixtures/blocks/core__gallery.json b/test/integration/fixtures/blocks/core__gallery.json index 8b7a1000d37ccc..27fc4c75dc8dc0 100644 --- a/test/integration/fixtures/blocks/core__gallery.json +++ b/test/integration/fixtures/blocks/core__gallery.json @@ -8,6 +8,7 @@ "shortCodeTransforms": [], "caption": "", "imageCrop": true, + "randomOrder": false, "fixedHeight": true, "linkTo": "none", "sizeSlug": "large", diff --git a/test/integration/fixtures/blocks/core__gallery__columns.json b/test/integration/fixtures/blocks/core__gallery__columns.json index d0c40b3d3a9a93..beb97c3ac50802 100644 --- a/test/integration/fixtures/blocks/core__gallery__columns.json +++ b/test/integration/fixtures/blocks/core__gallery__columns.json @@ -9,6 +9,7 @@ "columns": 1, "caption": "", "imageCrop": true, + "randomOrder": false, "fixedHeight": true, "linkTo": "none", "sizeSlug": "large", diff --git a/test/integration/fixtures/blocks/core__gallery__deprecated-7.json b/test/integration/fixtures/blocks/core__gallery__deprecated-7.json index 4eaf85b46906d6..05a630370f9b09 100644 --- a/test/integration/fixtures/blocks/core__gallery__deprecated-7.json +++ b/test/integration/fixtures/blocks/core__gallery__deprecated-7.json @@ -8,6 +8,7 @@ "shortCodeTransforms": [], "caption": "", "imageCrop": true, + "randomOrder": false, "fixedHeight": true, "linkTo": "media", "sizeSlug": "large", diff --git a/test/integration/fixtures/blocks/core__post-featured-image.json b/test/integration/fixtures/blocks/core__post-featured-image.json index 158007533a3f2b..dec6e14712a3a2 100644 --- a/test/integration/fixtures/blocks/core__post-featured-image.json +++ b/test/integration/fixtures/blocks/core__post-featured-image.json @@ -7,7 +7,8 @@ "scale": "cover", "rel": "", "linkTarget": "_self", - "dimRatio": 0 + "dimRatio": 0, + "useFirstImageFromPost": false }, "innerBlocks": [] } diff --git a/test/integration/fixtures/blocks/core__search.json b/test/integration/fixtures/blocks/core__search.json index f692eac10993d8..ec961ed41b0244 100644 --- a/test/integration/fixtures/blocks/core__search.json +++ b/test/integration/fixtures/blocks/core__search.json @@ -8,7 +8,6 @@ "buttonPosition": "button-outside", "buttonUseIcon": false, "query": {}, - "buttonBehavior": "expand-searchfield", "isSearchFieldHidden": false }, "innerBlocks": [] diff --git a/test/integration/fixtures/blocks/core__search__custom-text.json b/test/integration/fixtures/blocks/core__search__custom-text.json index c763cb60f65e86..3738816762ba1e 100644 --- a/test/integration/fixtures/blocks/core__search__custom-text.json +++ b/test/integration/fixtures/blocks/core__search__custom-text.json @@ -10,7 +10,6 @@ "buttonPosition": "button-outside", "buttonUseIcon": false, "query": {}, - "buttonBehavior": "expand-searchfield", "isSearchFieldHidden": false }, "innerBlocks": [] diff --git a/test/native/setup.js b/test/native/setup.js index bf6c6d970aa320..4fa76845d6e8e0 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -283,3 +283,9 @@ jest.mock( '@wordpress/compose', () => { jest.spyOn( Image, 'getSize' ).mockImplementation( ( url, success ) => success( 0, 0 ) ); + +jest.mock( 'react-native/Libraries/Utilities/BackHandler', () => { + return jest.requireActual( + 'react-native/Libraries/Utilities/__mocks__/BackHandler.js' + ); +} );