From 102405a6677de2698c2eefe2ac42f181662472ef Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Thu, 2 Jan 2025 14:34:43 +0000 Subject: [PATCH] Merge changes published in the Gutenberg plugin "release/20.0" branch --- .eslintrc.js | 5 + .github/CODEOWNERS | 2 +- .../workflows/sync-assets-to-plugin-repo.yml | 48 + .../upload-release-to-plugin-repo.yml | 27 +- LICENSE.md | 2 +- assets/README.md | 7 + assets/banner-1544x500.jpg | Bin 0 -> 363052 bytes assets/banner-772x250.jpg | Bin 0 -> 108852 bytes assets/blueprints/blueprint.json | 28 + assets/icon-128x128.jpg | Bin 0 -> 37198 bytes assets/icon-256x256.jpg | Bin 0 -> 51683 bytes backport-changelog/6.8/7069.md | 6 - backport-changelog/6.8/7129.md | 1 + backport-changelog/6.8/7865.md | 3 +- backport-changelog/6.8/7898.md | 3 + backport-changelog/6.8/8014.md | 3 + backport-changelog/6.8/8015.md | 3 + backport-changelog/6.8/8031.md | 4 + backport-changelog/6.8/8032.md | 3 + .../get-tags-from-storybook.mjs | 30 + bin/api-docs/gen-components-docs/index.mjs | 9 + .../gen-components-docs/markdown/index.mjs | 27 +- .../gen-components-docs/markdown/props.mjs | 3 +- bin/check-licenses.mjs | 2 +- bin/plugin/lib/utils.js | 19 +- bin/test-create-block.sh | 4 +- bin/tsconfig.json | 1 + bin/validate-tsconfig.mjs | 35 +- changelog.txt | 840 +++ docs/README.md | 2 +- docs/explanations/architecture/styles.md | 16 +- .../devenv/get-started-with-wp-env.md | 2 +- docs/getting-started/faq.md | 2 +- .../javascript-in-the-block-editor.md | 6 +- .../fundamentals/registration-of-a-block.md | 2 +- .../nested-blocks-inner-blocks.md | 2 +- .../themes/global-settings-and-styles.md | 101 +- docs/private-apis.md | 340 + .../block-api/block-edit-save.md | 27 +- docs/reference-guides/core-blocks.md | 2 +- .../data/data-core-block-editor.md | 8 +- .../reference-guides/data/data-core-blocks.md | 2 +- .../data/data-core-edit-post.md | 2 +- .../data/data-core-rich-text.md | 2 +- docs/tool/manifest.js | 1 + gutenberg.php | 2 +- jsconfig.json | 15 - lib/block-supports/block-style-variations.php | 6 +- lib/block-supports/border.php | 28 +- lib/block-supports/elements.php | 8 +- lib/block-supports/layout.php | 6 +- lib/block-supports/settings.php | 4 +- lib/block-supports/typography.php | 24 +- lib/class-wp-duotone-gutenberg.php | 4 +- lib/class-wp-theme-json-gutenberg.php | 10 +- lib/compat/wordpress-6.8/blocks.php | 284 +- .../class-gutenberg-hierarchical-sort.php | 205 + lib/compat/wordpress-6.8/post.php | 9 +- lib/compat/wordpress-6.8/site-editor.php | 2 +- .../bc-layer/webfonts-deprecations.php | 2 +- lib/experimental/kses-allowed-html.php | 2 +- lib/experimental/media/load.php | 2 + lib/experimental/posts/load.php | 2 +- lib/load.php | 1 + lib/rest-api.php | 2 +- lib/theme-i18n.json | 19 + package-lock.json | 5622 ++++------------- package.json | 43 +- packages/a11y/tsconfig.json | 7 +- packages/api-fetch/tsconfig.json | 7 +- packages/autop/tsconfig.json | 7 +- packages/blob/tsconfig.json | 7 +- packages/block-editor/README.md | 40 + .../components/audio-player/index.native.js | 4 +- .../background-image-control/index.js | 4 + .../background-image-control/style.scss | 6 +- .../block-alignment-matrix-control/README.md | 35 +- .../block-alignment-matrix-control/index.js | 31 + .../stories/index.story.js | 78 + .../src/components/block-card/README.md | 7 + .../src/components/block-card/index.js | 53 +- .../block-card/stories/index.story.js | 79 + .../src/components/block-card/style.scss | 10 +- .../src/components/block-edit/edit.js | 229 +- .../src/components/block-list/index.js | 9 +- .../block-list/use-block-props/index.js | 2 +- .../block-list/zoom-out-separator.js | 30 + .../block-settings-dropdown.js | 37 +- .../src/components/block-styles/utils.js | 6 +- .../block-title/stories/index.story.js | 76 + .../src/components/block-tools/style.scss | 1 + .../block-tools/zoom-out-mode-inserters.js | 24 +- .../border-radius-control/README.md | 59 + .../stories/index.story.js | 58 + .../components/button-block-appender/index.js | 9 +- .../components/child-layout-control/index.js | 14 + .../components/colors-gradients/dropdown.js | 65 +- .../components/colors-gradients/style.scss | 5 + .../contrast-checker/index.native.js | 4 +- .../contrast-checker/stories/index.story.js | 117 + .../components/date-format-picker/README.md | 19 +- .../components/date-format-picker/index.js | 27 +- .../date-format-picker/stories/index.story.js | 69 + .../components/date-format-picker/style.scss | 4 +- .../default-block-appender/content.scss | 1 + .../stories/aspect-ratio-tool.story.js | 3 +- .../dimensions-tool/stories/index.story.js | 3 +- .../stories/scale-tool.story.js | 3 +- .../stories/width-height-tool.story.js | 3 +- .../font-appearance-control/index.js | 15 + .../src/components/font-family/README.md | 1 + .../src/components/font-family/index.js | 18 +- .../font-family/stories/index.story.js | 1 + .../components/global-styles/color-panel.js | 3 + .../test/use-global-styles-output.js | 2 +- .../global-styles/typography-utils.js | 2 +- .../global-styles/use-global-styles-output.js | 4 +- .../src/components/grid/grid-visualizer.js | 11 + .../block-editor/src/components/grid/utils.js | 20 +- .../src/components/iframe/index.js | 2 +- .../use-inner-block-template-sync.js | 27 +- .../inserter/block-patterns-explorer/index.js | 5 +- .../inserter/block-patterns-tab/index.js | 4 +- .../pattern-category-previews.js | 8 +- .../block-patterns-tab/patterns-filter.js | 4 +- .../inserter/category-tabs/index.js | 5 +- .../src/components/inserter/index.js | 14 +- .../src/components/inserter/quick-inserter.js | 27 +- .../components/keyboard-shortcuts/index.js | 4 +- .../components/line-height-control/README.md | 1 + .../components/line-height-control/index.js | 12 + .../stories/index.story.js | 1 + .../line-height-control/test/index.js | 8 +- .../src/components/list-view/style.scss | 15 +- .../src/components/media-placeholder/index.js | 6 +- .../components/media-replace-flow/README.md | 7 + .../components/media-replace-flow/index.js | 64 +- .../components/media-replace-flow/style.scss | 12 +- .../src/components/plain-text/README.md | 4 +- .../src/components/plain-text/index.js | 34 + .../plain-text/stories/index.story.js | 75 + .../src/components/provider/index.js | 85 +- .../provider/use-media-upload-settings.js | 25 + .../src/components/resolution-tool/index.js | 2 + .../resolution-tool/stories/index.story.js | 44 +- .../responsive-block-control/index.js | 2 +- .../rich-text/event-listeners/delete.js | 7 +- .../src/components/rich-text/index.js | 2 +- .../text-alignment-control/README.md | 49 + .../stories/index.story.js | 74 +- .../text-decoration-control/README.md | 1 - .../stories/index.story.js | 67 +- .../text-transform-control/README.md | 7 +- .../stories/index.story.js | 69 +- .../components/use-block-drop-zone/index.js | 9 +- .../components/warning/stories/index.story.js | 86 + packages/block-editor/src/hooks/background.js | 6 + .../block-editor/src/hooks/block-bindings.js | 35 +- packages/block-editor/src/hooks/border.js | 12 +- packages/block-editor/src/hooks/color.js | 2 +- packages/block-editor/src/hooks/dimensions.js | 4 +- .../block-editor/src/hooks/font-family.js | 2 +- packages/block-editor/src/hooks/gap.js | 4 +- packages/block-editor/src/hooks/index.js | 1 - .../block-editor/src/hooks/index.native.js | 1 + packages/block-editor/src/hooks/style.js | 20 +- packages/block-editor/src/hooks/supports.js | 14 +- packages/block-editor/src/hooks/test/style.js | 3 +- packages/block-editor/src/hooks/typography.js | 12 +- .../src/hooks/use-bindings-attributes.js | 322 - .../block-editor/src/hooks/use-zoom-out.js | 12 +- packages/block-editor/src/hooks/utils.js | 2 +- .../block-editor/src/store/private-actions.js | 1 + .../src/store/private-selectors.js | 20 +- packages/block-editor/src/store/reducer.js | 131 +- packages/block-editor/src/store/selectors.js | 14 +- .../src/store/test/private-selectors.js | 62 +- .../block-editor/src/store/test/reducer.js | 338 +- .../block-editor/src/store/test/selectors.js | 23 + .../block-editor/src/utils/block-bindings.js | 95 + packages/block-editor/tsconfig.json | 7 +- packages/block-library/src/archives/edit.js | 130 +- .../test/__snapshots__/edit.native.js.snap | 2 +- packages/block-library/src/block/index.php | 20 + packages/block-library/src/button/block.json | 19 +- .../block-library/src/button/deprecated.js | 189 + packages/block-library/src/button/edit.js | 91 +- packages/block-library/src/button/save.js | 7 +- packages/block-library/src/column/edit.js | 47 +- packages/block-library/src/columns/edit.js | 111 +- packages/block-library/src/comments/index.js | 1 + .../block-library/src/cover/edit.native.js | 7 +- .../block-library/src/cover/edit/index.js | 17 +- packages/block-library/src/cover/test/edit.js | 12 +- packages/block-library/src/details/edit.js | 51 +- packages/block-library/src/editor.scss | 1 - .../test/__snapshots__/edit.native.js.snap | 2 +- .../block-library/src/image/transforms.js | 1 + .../block-library/src/latest-posts/block.json | 12 + packages/block-library/src/loginout/edit.js | 76 +- packages/block-library/src/media-text/edit.js | 85 +- packages/block-library/src/more/edit.js | 51 +- .../block-library/src/navigation-link/edit.js | 162 +- .../src/navigation-link/index.php | 17 +- .../src/navigation-submenu/edit.js | 170 +- .../edit/navigation-menu-selector.js | 11 +- .../block-library/src/navigation/index.php | 4 +- packages/block-library/src/page-list/edit.js | 83 +- packages/block-library/src/paragraph/edit.js | 22 +- .../src/post-author-name/block.json | 6 +- .../src/post-author-name/edit.js | 21 +- .../src/post-author-name/index.php | 4 + .../block-library/src/post-author/block.json | 6 +- .../block-library/src/post-author/edit.js | 20 +- .../block-library/src/post-author/index.php | 4 + .../src/post-comments-form/block.json | 7 +- .../src/post-comments-link/block.json | 9 +- .../src/post-comments-link/style.scss | 4 + .../block-library/src/post-content/index.php | 23 + .../block-library/src/post-date/block.json | 3 +- packages/block-library/src/post-date/edit.js | 104 +- .../block-library/src/post-excerpt/edit.js | 81 +- .../src/post-featured-image/block.json | 9 +- .../post-featured-image/dimension-controls.js | 39 +- .../src/post-featured-image/edit.js | 77 +- .../src/post-navigation-link/block.json | 6 - .../src/post-navigation-link/index.js | 11 + .../src/post-navigation-link/variations.js | 4 +- .../src/post-template/block.json | 12 +- .../src/post-template/editor.scss | 7 - .../src/post-template/style.scss | 2 + .../block-library/src/post-title/block.json | 9 +- .../src/query-no-results/block.json | 10 - .../src/query-no-results/index.js | 11 + .../src/query-pagination-numbers/edit.js | 60 +- .../src/query-pagination-previous/index.php | 35 +- .../src/query-pagination/edit.js | 72 +- .../query-pagination-label-control.js | 4 +- .../block-library/src/query-total/block.json | 9 +- .../block-library/src/query-total/edit.js | 24 +- .../block-library/src/query-total/index.php | 22 +- .../block-library/src/query-total/style.scss | 4 + packages/block-library/src/read-more/index.js | 6 + .../block-library/src/site-logo/block.json | 6 +- .../block-library/src/site-title/block.json | 6 +- packages/block-library/src/site-title/edit.js | 67 +- .../block-library/src/social-link/edit.js | 27 +- .../block-library/src/social-links/edit.js | 67 +- packages/block-library/src/spacer/controls.js | 57 +- packages/block-library/src/style.scss | 2 + .../src/table-of-contents/block.json | 52 - .../src/table-of-contents/edit.js | 2 +- .../src/table-of-contents/index.js | 53 + packages/block-library/src/table/block.json | 5 +- packages/block-library/src/table/edit.js | 78 +- packages/block-library/src/tag-cloud/edit.js | 68 +- .../block-library/src/tag-cloud/editor.scss | 8 - .../src/video/edit-common-settings.js | 136 +- packages/block-library/src/video/edit.js | 104 +- .../block-library/src/video/poster-image.js | 86 + .../block-library/src/video/tracks-editor.js | 2 +- packages/block-library/tsconfig.json | 2 - .../tsconfig.json | 7 +- packages/blocks/src/api/constants.js | 64 +- packages/blocks/src/api/factory.js | 55 +- packages/blocks/src/api/templates.js | 13 +- packages/blocks/src/api/test/utils.js | 15 + packages/blocks/src/api/utils.js | 23 + .../blocks/src/store/process-block-type.js | 217 +- packages/blocks/src/store/selectors.js | 2 +- .../src/store/test/private-selectors.js | 12 +- .../src/store/test/process-block-type.js | 490 -- packages/components/CHANGELOG.md | 38 +- .../src/alignment-matrix-control/README.md | 31 +- .../src/angle-picker-control/README.md | 19 +- packages/components/src/badge/README.md | 24 + .../components/src/badge/docs-manifest.json | 5 + packages/components/src/badge/index.tsx | 67 + .../src/badge/stories/index.story.tsx | 54 + packages/components/src/badge/styles.scss | 49 + packages/components/src/badge/test/index.tsx | 45 + packages/components/src/badge/types.ts | 12 + .../components/src/base-control/README.md | 43 +- packages/components/src/box-control/README.md | 65 +- packages/components/src/box-control/index.tsx | 4 + .../src/box-control/input-control.tsx | 182 +- .../src/box-control/stories/index.story.tsx | 12 + packages/components/src/box-control/types.ts | 18 + packages/components/src/box-control/utils.ts | 60 + .../components/src/button-group/README.md | 4 + .../components/src/button-group/index.tsx | 12 +- .../src/button-group/stories/index.story.tsx | 9 +- packages/components/src/button-group/types.ts | 7 + packages/components/src/button/README.md | 101 +- packages/components/src/button/style.scss | 19 +- packages/components/src/button/test/index.tsx | 11 +- .../custom-select.tsx | 3 +- .../stories/index.story.tsx | 4 + .../src/custom-select-control/index.tsx | 6 +- .../components/src/date-time/date/index.tsx | 1 + .../src/dimension-control/index.tsx | 1 + .../test/__snapshots__/index.test.js.snap | 8 +- .../src/disabled/stories/index.story.tsx | 1 + .../src/drop-zone/stories/index.story.tsx | 8 +- .../components/src/font-size-picker/styles.ts | 1 + .../components/src/form-file-upload/README.md | 43 +- .../components/src/gradient-picker/README.md | 71 +- packages/components/src/heading/hook.ts | 2 +- .../heading/test/__snapshots__/index.tsx.snap | 8 +- packages/components/src/icon/README.md | 17 +- .../components/src/input-control/README.md | 1 + .../components/src/input-control/index.tsx | 10 + .../src/input-control/stories/index.story.tsx | 7 +- .../src/input-control/test/index.js | 12 +- .../components/src/input-control/types.ts | 13 +- .../components/src/menu/checkbox-item.tsx | 3 +- packages/components/src/menu/index.tsx | 226 +- packages/components/src/menu/item.tsx | 19 +- packages/components/src/menu/popover.tsx | 103 + packages/components/src/menu/radio-item.tsx | 3 +- .../src/menu/stories/index.story.tsx | 914 +-- .../src/menu/submenu-trigger-item.tsx | 61 + packages/components/src/menu/test/index.tsx | 448 +- .../components/src/menu/trigger-button.tsx | 46 + packages/components/src/menu/types.ts | 273 +- .../src/modal/stories/index.story.tsx | 5 +- .../src/navigation/back-button/index.tsx | 1 + packages/components/src/navigation/index.tsx | 7 + .../components/src/navigation/item/index.tsx | 2 + .../components/src/navigation/test/index.tsx | 4 + .../components/src/navigator/test/index.tsx | 4 + .../components/src/number-control/index.tsx | 1 + .../components/src/palette-edit/index.tsx | 1 + .../src/panel/stories/index.story.tsx | 6 +- packages/components/src/private-apis.ts | 2 + packages/components/src/radio-group/index.tsx | 13 +- packages/components/src/radio-group/radio.tsx | 3 +- .../styles/range-control-styles.ts | 12 +- .../components/src/select-control/README.md | 3 + .../components/src/select-control/index.tsx | 10 + .../select-control/stories/index.story.tsx | 2 + .../select-control/test/select-control.tsx | 8 +- .../components/src/select-control/types.ts | 1 + packages/components/src/slot-fill/context.ts | 8 +- packages/components/src/slot-fill/fill.ts | 25 +- .../components/src/slot-fill/provider.tsx | 127 +- packages/components/src/slot-fill/slot.tsx | 67 +- packages/components/src/slot-fill/types.ts | 34 +- packages/components/src/slot-fill/use-slot.ts | 27 - packages/components/src/style.scss | 1 + packages/components/src/tab-panel/index.tsx | 3 +- .../src/tab-panel/stories/index.story.tsx | 4 + packages/components/src/tabs/README.md | 338 +- .../components/src/tabs/docs-manifest.json | 22 + packages/components/src/tabs/index.tsx | 25 +- .../src/tabs/stories/best-practices.mdx | 99 + .../src/tabs/stories/index.story.tsx | 5 + packages/components/src/tabs/types.ts | 27 +- packages/components/src/text/hook.ts | 4 +- packages/components/src/text/styles.ts | 2 +- .../text/test/__snapshots__/index.tsx.snap | 6 +- packages/components/src/text/test/index.tsx | 2 +- .../test/__snapshots__/index.tsx.snap | 4 +- .../src/toggle-group-control/test/index.tsx | 8 +- .../toggle-group-control/as-radio-group.tsx | 3 +- packages/components/src/tooltip/index.tsx | 3 +- .../src/tree-grid/stories/index.story.tsx | 2 + packages/components/src/tree-select/README.md | 172 +- .../src/tree-select/docs-manifest.json | 5 + packages/components/src/tree-select/index.tsx | 13 +- .../src/tree-select/stories/index.story.tsx | 1 + packages/components/src/tree-select/types.ts | 9 +- packages/components/tsconfig.json | 3 - .../src/hooks/use-focus-return/index.js | 8 +- .../fetch/__experimental-fetch-url-data.js | 2 +- packages/core-data/src/private-selectors.ts | 26 +- packages/core-data/src/resolvers.js | 2 +- packages/core-data/tsconfig.json | 5 +- .../plugin-templates/$slug.php.mustache | 8 +- .../plugin-templates/readme.txt.mustache | 4 +- .../plugin-templates/$slug.php.mustache | 8 +- .../plugin-templates/readme.txt.mustache | 4 +- packages/create-block/CHANGELOG.md | 10 + .../create-block/docs/external-template.md | 6 +- .../lib/check-system-requirements.js | 14 +- packages/create-block/lib/index.js | 103 +- packages/create-block/lib/prompts.js | 39 +- packages/create-block/lib/scaffold.js | 14 +- packages/create-block/lib/templates.js | 47 +- .../lib/templates/es5/$slug.php.mustache | 8 +- .../lib/templates/es5/readme.txt.mustache | 4 +- .../lib/templates/plugin/$slug.php.mustache | 10 +- .../lib/templates/plugin/readme.txt.mustache | 4 +- packages/create-block/package.json | 2 +- .../src/components/header/style.scss | 13 +- packages/data-controls/tsconfig.json | 5 +- packages/data/README.md | 2 +- packages/data/src/registry.js | 2 +- packages/data/tsconfig.json | 5 +- packages/dataviews/CHANGELOG.md | 26 +- packages/dataviews/README.md | 484 +- packages/dataviews/package.json | 3 +- .../src/components/dataviews-context/index.ts | 3 + .../dataviews-filters/add-filter.tsx | 82 +- .../components/dataviews-filters/index.tsx | 2 +- .../components/dataviews-footer/style.scss | 3 - .../dataviews-item-actions/index.tsx | 52 +- .../src/components/dataviews-layout/index.tsx | 2 + .../dataviews-view-config/index.tsx | 95 +- .../dataviews-view-config/style.scss | 1 - .../src/components/dataviews/index.tsx | 16 +- .../src/components/dataviews/style.scss | 1 - .../form-field-visibility/index.tsx | 32 - .../src/dataviews-layouts/grid/index.tsx | 22 +- .../grid/preview-size-picker.tsx | 55 +- .../src/dataviews-layouts/grid/style.scss | 57 +- .../src/dataviews-layouts/list/index.tsx | 62 +- .../src/dataviews-layouts/list/style.scss | 10 +- .../table/column-header-menu.tsx | 300 +- .../table/column-primary.tsx | 7 + .../src/dataviews-layouts/table/index.tsx | 10 + .../src/dataviews-layouts/table/style.scss | 1 - packages/dataviews/src/test/dataform.tsx | 348 + packages/dataviews/src/test/dataviews.tsx | 380 ++ packages/dataviews/src/types.ts | 6 + packages/dataviews/tsconfig.json | 18 +- packages/date/tsconfig.json | 7 +- packages/deprecated/tsconfig.json | 7 +- packages/docgen/tsconfig.json | 6 +- packages/dom-ready/tsconfig.json | 7 +- packages/dom/src/dom/clean-node-list.js | 5 +- packages/dom/tsconfig.json | 3 - .../e2e-test-utils-playwright/tsconfig.json | 5 +- packages/e2e-test-utils/README.md | 6 +- packages/e2e-test-utils/src/create-user.js | 2 +- packages/e2e-test-utils/src/delete-theme.js | 2 +- packages/e2e-test-utils/src/install-theme.js | 2 +- packages/e2e-tests/plugins/block-bindings.php | 6 +- .../e2e-tests/plugins/block-bindings/index.js | 45 + .../directive-each/render.php | 51 + .../interactive-blocks/directive-each/view.js | 22 + .../assets/10x10_e2e_test_image_blue.png | Bin 0 -> 119 bytes .../router-styles-blue/block.json | 15 + .../router-styles-blue/render.php | 35 + .../router-styles-blue/style-from-link.css | 7 + .../router-styles-blue/style.css | 4 + .../assets/10x10_e2e_test_image_green.png | Bin 0 -> 119 bytes .../router-styles-green/block.json | 15 + .../router-styles-green/render.php | 35 + .../router-styles-green/style-from-link.css | 7 + .../router-styles-green/style.css | 4 + .../assets/10x10_e2e_test_image_red.png | Bin 0 -> 119 bytes .../router-styles-red/block.json | 15 + .../router-styles-red/render.php | 35 + .../router-styles-red/style-from-link.css | 7 + .../router-styles-red/style.css | 4 + .../router-styles-wrapper/block.json | 16 + .../router-styles-wrapper/render.php | 70 + .../router-styles-wrapper/style.css | 3 + .../router-styles-wrapper/view.asset.php | 9 + .../router-styles-wrapper/view.js | 20 + .../edit-post/src/components/layout/index.js | 12 +- packages/edit-post/src/store/selectors.js | 2 +- packages/edit-site/package.json | 1 + .../src/components/add-new-pattern/index.js | 17 +- .../src/components/canvas-loader/style.scss | 7 +- .../editor-canvas-container/style.scss | 8 +- .../edit-site/src/components/editor/index.js | 8 +- .../src/components/editor/style.scss | 6 +- .../confirm-reset-shadow-dialog.js | 37 + .../global-styles/font-library-modal/index.js | 2 +- .../font-library-modal/style.scss | 6 +- .../global-styles/font-sizes/font-size.js | 47 +- .../global-styles/font-sizes/font-sizes.js | 72 +- .../components/global-styles/screen-block.js | 5 +- .../global-styles/shadows-edit-panel.js | 66 +- .../components/global-styles/shadows-panel.js | 69 +- .../src/components/global-styles/style.scss | 10 +- .../global-styles/variations/style.scss | 7 +- .../edit-site/src/components/layout/index.js | 13 +- .../src/components/layout/style.scss | 11 +- .../src/components/page-patterns/fields.js | 52 +- .../src/components/page-patterns/style.scss | 29 +- .../src/components/page-templates/fields.js | 22 +- .../src/components/page-templates/style.scss | 24 +- .../edit-site/src/components/page/style.scss | 13 +- .../src/components/post-list/index.js | 17 +- .../custom-dataviews-list.js | 13 +- .../sidebar-dataviews/default-views.js | 5 +- .../components/sidebar-dataviews/style.scss | 5 +- .../sidebar-global-styles-wrapper/index.js | 120 +- .../sidebar-global-styles-wrapper/style.scss | 22 + .../sidebar-navigation-item/style.scss | 6 + .../sidebar-navigation-screen-main/index.js | 2 +- .../site-editor-routes/stylebook.js | 4 +- .../components/site-editor-routes/styles.js | 6 +- .../src/components/site-hub/style.scss | 6 +- .../src/components/style-book/constants.ts | 50 +- .../src/components/style-book/index.js | 228 +- .../src/components/layout/style.scss | 7 - .../secondary-sidebar/inserter-sidebar.js | 13 +- .../edit-widgets/src/store/transformers.js | 2 +- packages/editor/README.md | 1 + .../editor/src/components/commands/index.js | 29 + .../src/components/document-bar/index.js | 3 +- .../src/components/document-tools/index.js | 9 +- .../src/components/document-tools/style.scss | 6 - .../editor-help/intro-to-blocks.native.js | 2 +- .../components/entities-saved-states/index.js | 6 +- .../components/error-boundary/index.native.js | 4 +- .../editor/src/components/more-menu/index.js | 1 - .../src/components/post-actions/actions.js | 16 +- .../src/components/post-actions/index.js | 36 +- .../post-actions/set-as-homepage.js | 74 +- .../post-actions/set-as-posts-page.js | 164 + .../src/components/post-card-panel/index.js | 14 +- .../src/components/post-card-panel/style.scss | 17 +- .../post-publish-panel/maybe-upload-media.js | 4 +- .../src/components/preferences-modal/index.js | 21 +- .../src/components/preview-dropdown/index.js | 1 - .../disable-non-page-content-blocks.js | 77 +- .../editor/src/components/provider/index.js | 40 +- .../provider/use-block-editor-settings.js | 3 + .../components/start-page-options/index.js | 147 +- .../template-part-menu-items/index.js | 13 +- packages/editor/src/store/actions.js | 2 - packages/editor/src/store/constants.ts | 2 - packages/editor/src/store/private-actions.js | 2 +- .../editor/src/store/utils/notice-builder.js | 11 +- .../src/store/utils/test/notice-builder.js | 7 +- packages/editor/src/utils/get-item-title.js | 25 + .../editor/src/utils/media-sideload/index.js | 13 + .../src/utils/media-sideload/index.native.js | 1 + .../editor/src/utils/media-upload/index.js | 5 +- packages/editor/tsconfig.json | 5 +- packages/element/tsconfig.json | 6 +- packages/env/CHANGELOG.md | 10 +- packages/env/lib/commands/destroy.js | 19 +- packages/env/lib/commands/start.js | 20 +- packages/env/lib/config/parse-config.js | 6 + .../__snapshots__/config-integration.js.snap | 8 + packages/env/lib/config/test/parse-config.js | 1 + packages/env/lib/wordpress.js | 31 +- packages/env/package.json | 2 +- packages/escape-html/tsconfig.json | 7 +- packages/eslint-plugin/tsconfig.json | 6 +- .../src/actions/permanently-delete-post.tsx | 243 +- .../create-template-part-modal/index.tsx | 106 +- .../create-template-part-modal/style.scss | 133 +- .../fields/src/fields/page-title/style.scss | 10 - .../fields/src/fields/page-title/view.tsx | 7 +- packages/fields/src/style.scss | 1 - packages/fields/tsconfig.json | 5 +- packages/hooks/tsconfig.json | 5 +- packages/html-entities/tsconfig.json | 7 +- packages/i18n/tsconfig.json | 7 +- packages/icons/CHANGELOG.md | 4 + .../icons/src/icon/stories/index.story.js | 9 +- packages/icons/src/icon/stories/keywords.ts | 4 +- packages/icons/src/index.js | 8 +- packages/icons/src/library/caution-filled.js | 12 + packages/icons/src/library/caution.js | 16 + packages/icons/src/library/error.js | 16 + packages/icons/src/library/info.js | 8 +- packages/icons/src/library/warning.js | 12 - packages/icons/tsconfig.json | 5 - .../interactivity-router/src/assets/styles.ts | 79 + packages/interactivity-router/src/head.ts | 126 - packages/interactivity-router/src/index.ts | 61 +- packages/interactivity-router/tsconfig.json | 5 +- packages/interactivity/CHANGELOG.md | 8 + packages/interactivity/src/directives.tsx | 27 +- packages/interactivity/src/hooks.tsx | 2 +- packages/interactivity/tsconfig.json | 6 +- packages/interactivity/tsconfig.test.json | 2 +- packages/interface/CHANGELOG.md | 4 + .../src/components/action-item/README.md | 4 +- .../src/components/action-item/index.js | 4 +- packages/is-shallow-equal/tsconfig.json | 7 +- packages/keycodes/tsconfig.json | 7 +- packages/lazy-import/tsconfig.json | 3 +- packages/media-utils/src/utils/types.ts | 1 - .../media-utils/src/utils/upload-media.ts | 27 +- packages/media-utils/tsconfig.json | 3 - packages/notices/tsconfig.json | 5 +- packages/plugins/tsconfig.json | 5 +- packages/postcss-plugins-preset/CHANGELOG.md | 4 + packages/postcss-plugins-preset/package.json | 2 +- packages/prettier-config/tsconfig.json | 3 +- packages/primitives/tsconfig.json | 5 - packages/priority-queue/tsconfig.json | 5 +- packages/private-apis/src/implementation.ts | 1 + packages/private-apis/tsconfig.json | 5 +- .../tsconfig.json | 3 +- packages/react-i18n/tsconfig.json | 7 +- packages/react-native-editor/ios/Gemfile.lock | 8 +- packages/redux-routine/tsconfig.json | 7 +- packages/report-flaky-tests/tsconfig.json | 5 +- packages/rich-text/src/store/selectors.js | 2 +- packages/rich-text/tsconfig.json | 5 +- packages/router/tsconfig.json | 5 +- packages/scripts/CHANGELOG.md | 10 + packages/scripts/README.md | 163 +- packages/scripts/config/webpack.config.js | 20 +- packages/scripts/package.json | 4 +- .../php-file-paths-plugin/index.js} | 4 +- .../plugins/rtlcss-webpack-plugin/index.js | 66 + packages/scripts/scripts/build.js | 22 +- packages/scripts/scripts/start.js | 26 +- packages/scripts/utils/config.js | 86 +- packages/scripts/utils/index.js | 10 +- packages/server-side-render/README.md | 2 +- packages/shortcode/tsconfig.json | 6 +- packages/style-engine/tsconfig.json | 7 +- packages/sync/tsconfig.json | 3 - packages/token-list/tsconfig.json | 7 +- packages/undo-manager/tsconfig.json | 5 +- packages/upload-media/CHANGELOG.md | 5 + packages/upload-media/README.md | 136 + packages/upload-media/package.json | 45 + .../src/components/provider/index.tsx | 25 + .../provider/with-registry-provider.tsx | 59 + .../upload-media/src/get-mime-types-array.ts | 29 + packages/upload-media/src/image-file.ts | 38 + packages/upload-media/src/index.ts | 11 + packages/upload-media/src/lock-unlock.ts | 10 + packages/upload-media/src/store/actions.ts | 183 + packages/upload-media/src/store/constants.ts | 1 + packages/upload-media/src/store/index.ts | 43 + .../upload-media/src/store/private-actions.ts | 407 ++ .../src/store/private-selectors.ts | 113 + packages/upload-media/src/store/reducer.ts | 195 + packages/upload-media/src/store/selectors.ts | 67 + .../upload-media/src/store/test/actions.ts | 112 + .../upload-media/src/store/test/reducer.ts | 279 + .../upload-media/src/store/test/selectors.ts | 105 + packages/upload-media/src/store/types.ts | 172 + packages/upload-media/src/stub-file.ts | 5 + .../src/test/get-file-basename.ts | 15 + .../src/test/get-file-extension.ts | 15 + .../src/test/get-file-name-from-url.ts | 14 + .../src/test/get-mime-types-array.ts | 47 + packages/upload-media/src/test/image-file.ts | 15 + .../upload-media/src/test/upload-error.ts | 24 + .../src/test/validate-file-size.ts | 70 + .../src/test/validate-mime-type-for-user.ts | 37 + .../src/test/validate-mime-type.ts | 57 + packages/upload-media/src/upload-error.ts | 26 + packages/upload-media/src/utils.ts | 90 + .../upload-media/src/validate-file-size.ts | 44 + .../src/validate-mime-type-for-user.ts | 46 + .../upload-media/src/validate-mime-type.ts | 43 + packages/upload-media/tsconfig.json | 17 + packages/url/tsconfig.json | 7 +- packages/vips/tsconfig.json | 7 +- packages/warning/tsconfig.json | 5 +- packages/wordcount/tsconfig.json | 7 +- patches/storybook-source-link+2.0.9.patch | 55 + phpunit/block-supports/border-test.php | 381 +- phpunit/block-supports/typography-test.php | 105 - ...class-gutenberg-hierarchical-sort-test.php | 207 + platform-docs/docs/intro.md | 83 +- schemas/json/theme.json | 36 +- schemas/json/wp-env.json | 16 +- .../decorators/with-max-width-wrapper.js | 30 +- storybook/main.js | 6 +- storybook/preview.js | 12 +- storybook/sidebar.js | 6 +- storybook/stories/docs/inline-icon.js | 10 +- storybook/stories/docs/introduction.mdx | 3 +- storybook/stories/foundations/layout.mdx | 60 +- storybook/stories/playground/box/index.js | 2 +- .../playground/with-undo-redo/index.js | 2 +- .../stories/playground/zoom-out/index.js | 2 +- test/e2e/playwright.config.ts | 2 +- test/e2e/specs/editor/blocks/buttons.spec.js | 14 +- test/e2e/specs/editor/blocks/image.spec.js | 3 - .../specs/editor/blocks/navigation.spec.js | 10 +- .../various/block-bindings/post-meta.spec.js | 41 + .../editor/various/block-deletion.spec.js | 2 +- .../editor/various/block-locking.spec.js | 2 +- .../specs/editor/various/list-view.spec.js | 12 +- .../various/template-resolution.spec.js | 4 + .../editor/various/write-design-mode.spec.js | 11 + ...-in-referenced-style-sheets-1-chromium.png | Bin 0 -> 92 bytes ...-in-referenced-style-sheets-2-chromium.png | Bin 0 -> 96 bytes ...-in-referenced-style-sheets-3-chromium.png | Bin 0 -> 96 bytes ...-in-referenced-style-sheets-4-chromium.png | Bin 0 -> 97 bytes ...-in-referenced-style-sheets-5-chromium.png | Bin 0 -> 97 bytes .../interactivity/directive-each.spec.ts | 35 +- .../e2e/specs/interactivity/fixtures/index.ts | 4 +- .../fixtures/interactivity-utils.ts | 34 +- .../specs/interactivity/router-styles.spec.ts | 232 + .../block-style-variations.spec.js | 4 +- .../site-editor/homepage-settings.spec.js | 56 +- test/e2e/specs/site-editor/page-list.spec.js | 376 +- test/e2e/specs/site-editor/pages.spec.js | 3 + .../specs/site-editor/template-part.spec.js | 2 +- .../site-editor/template-registration.spec.js | 5 +- test/e2e/specs/site-editor/zoom-out.spec.js | 33 + .../e2e/specs/widgets/editing-widgets.spec.js | 2 +- test/e2e/tsconfig.json | 8 +- ...re__button__deprecated-v10.serialized.html | 2 +- .../blocks/core__button__deprecated-v12.html | 15 + .../blocks/core__button__deprecated-v12.json | 59 + .../core__button__deprecated-v12.parsed.json | 81 + ...re__button__deprecated-v12.serialized.html | 15 + test/performance/playwright.config.ts | 2 +- test/performance/specs/site-editor.spec.js | 2 +- test/performance/tsconfig.json | 8 +- test/storybook-playwright/storybook/main.js | 5 +- test/unit/config/global-mocks.js | 4 - tools/webpack/blocks.js | 2 +- tsconfig.base.json | 6 +- tsconfig.json | 5 +- 715 files changed, 18984 insertions(+), 11574 deletions(-) create mode 100644 .github/workflows/sync-assets-to-plugin-repo.yml create mode 100644 assets/README.md create mode 100644 assets/banner-1544x500.jpg create mode 100644 assets/banner-772x250.jpg create mode 100644 assets/blueprints/blueprint.json create mode 100644 assets/icon-128x128.jpg create mode 100644 assets/icon-256x256.jpg delete mode 100644 backport-changelog/6.8/7069.md create mode 100644 backport-changelog/6.8/7898.md create mode 100644 backport-changelog/6.8/8014.md create mode 100644 backport-changelog/6.8/8015.md create mode 100644 backport-changelog/6.8/8031.md create mode 100644 backport-changelog/6.8/8032.md create mode 100644 bin/api-docs/gen-components-docs/get-tags-from-storybook.mjs create mode 100644 docs/private-apis.md delete mode 100644 jsconfig.json create mode 100644 lib/compat/wordpress-6.8/class-gutenberg-hierarchical-sort.php create mode 100644 packages/block-editor/src/components/block-alignment-matrix-control/stories/index.story.js create mode 100644 packages/block-editor/src/components/block-card/stories/index.story.js create mode 100644 packages/block-editor/src/components/block-title/stories/index.story.js create mode 100644 packages/block-editor/src/components/border-radius-control/README.md create mode 100644 packages/block-editor/src/components/border-radius-control/stories/index.story.js create mode 100644 packages/block-editor/src/components/contrast-checker/stories/index.story.js create mode 100644 packages/block-editor/src/components/date-format-picker/stories/index.story.js create mode 100644 packages/block-editor/src/components/plain-text/stories/index.story.js create mode 100644 packages/block-editor/src/components/provider/use-media-upload-settings.js create mode 100644 packages/block-editor/src/components/text-alignment-control/README.md create mode 100644 packages/block-editor/src/components/warning/stories/index.story.js delete mode 100644 packages/block-editor/src/hooks/use-bindings-attributes.js create mode 100644 packages/block-library/src/post-comments-link/style.scss delete mode 100644 packages/block-library/src/post-template/editor.scss create mode 100644 packages/block-library/src/query-total/style.scss create mode 100644 packages/block-library/src/video/poster-image.js delete mode 100644 packages/blocks/src/store/test/process-block-type.js create mode 100644 packages/components/src/badge/README.md create mode 100644 packages/components/src/badge/docs-manifest.json create mode 100644 packages/components/src/badge/index.tsx create mode 100644 packages/components/src/badge/stories/index.story.tsx create mode 100644 packages/components/src/badge/styles.scss create mode 100644 packages/components/src/badge/test/index.tsx create mode 100644 packages/components/src/badge/types.ts create mode 100644 packages/components/src/menu/popover.tsx create mode 100644 packages/components/src/menu/submenu-trigger-item.tsx create mode 100644 packages/components/src/menu/trigger-button.tsx delete mode 100644 packages/components/src/slot-fill/use-slot.ts create mode 100644 packages/components/src/tabs/docs-manifest.json create mode 100644 packages/components/src/tabs/stories/best-practices.mdx create mode 100644 packages/components/src/tree-select/docs-manifest.json delete mode 100644 packages/dataviews/src/components/form-field-visibility/index.tsx create mode 100644 packages/dataviews/src/test/dataform.tsx create mode 100644 packages/dataviews/src/test/dataviews.tsx create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/assets/10x10_e2e_test_image_blue.png create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style-from-link.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/assets/10x10_e2e_test_image_green.png create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style-from-link.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/assets/10x10_e2e_test_image_red.png create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style-from-link.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/block.json create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/style.css create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.asset.php create mode 100644 packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js create mode 100644 packages/edit-site/src/components/global-styles/confirm-reset-shadow-dialog.js create mode 100644 packages/editor/src/components/post-actions/set-as-posts-page.js create mode 100644 packages/editor/src/utils/get-item-title.js create mode 100644 packages/editor/src/utils/media-sideload/index.js create mode 100644 packages/editor/src/utils/media-sideload/index.native.js delete mode 100644 packages/fields/src/fields/page-title/style.scss create mode 100644 packages/icons/src/library/caution-filled.js create mode 100644 packages/icons/src/library/caution.js create mode 100644 packages/icons/src/library/error.js delete mode 100644 packages/icons/src/library/warning.js create mode 100644 packages/interactivity-router/src/assets/styles.ts delete mode 100644 packages/interactivity-router/src/head.ts rename packages/scripts/{utils/php-file-paths-plugin.js => plugins/php-file-paths-plugin/index.js} (92%) create mode 100644 packages/scripts/plugins/rtlcss-webpack-plugin/index.js create mode 100644 packages/upload-media/CHANGELOG.md create mode 100644 packages/upload-media/README.md create mode 100644 packages/upload-media/package.json create mode 100644 packages/upload-media/src/components/provider/index.tsx create mode 100644 packages/upload-media/src/components/provider/with-registry-provider.tsx create mode 100644 packages/upload-media/src/get-mime-types-array.ts create mode 100644 packages/upload-media/src/image-file.ts create mode 100644 packages/upload-media/src/index.ts create mode 100644 packages/upload-media/src/lock-unlock.ts create mode 100644 packages/upload-media/src/store/actions.ts create mode 100644 packages/upload-media/src/store/constants.ts create mode 100644 packages/upload-media/src/store/index.ts create mode 100644 packages/upload-media/src/store/private-actions.ts create mode 100644 packages/upload-media/src/store/private-selectors.ts create mode 100644 packages/upload-media/src/store/reducer.ts create mode 100644 packages/upload-media/src/store/selectors.ts create mode 100644 packages/upload-media/src/store/test/actions.ts create mode 100644 packages/upload-media/src/store/test/reducer.ts create mode 100644 packages/upload-media/src/store/test/selectors.ts create mode 100644 packages/upload-media/src/store/types.ts create mode 100644 packages/upload-media/src/stub-file.ts create mode 100644 packages/upload-media/src/test/get-file-basename.ts create mode 100644 packages/upload-media/src/test/get-file-extension.ts create mode 100644 packages/upload-media/src/test/get-file-name-from-url.ts create mode 100644 packages/upload-media/src/test/get-mime-types-array.ts create mode 100644 packages/upload-media/src/test/image-file.ts create mode 100644 packages/upload-media/src/test/upload-error.ts create mode 100644 packages/upload-media/src/test/validate-file-size.ts create mode 100644 packages/upload-media/src/test/validate-mime-type-for-user.ts create mode 100644 packages/upload-media/src/test/validate-mime-type.ts create mode 100644 packages/upload-media/src/upload-error.ts create mode 100644 packages/upload-media/src/utils.ts create mode 100644 packages/upload-media/src/validate-file-size.ts create mode 100644 packages/upload-media/src/validate-mime-type-for-user.ts create mode 100644 packages/upload-media/src/validate-mime-type.ts create mode 100644 packages/upload-media/tsconfig.json create mode 100644 patches/storybook-source-link+2.0.9.patch create mode 100644 phpunit/class-gutenberg-hierarchical-sort-test.php create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-1-chromium.png create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-2-chromium.png create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-3-chromium.png create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-4-chromium.png create mode 100644 test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-5-chromium.png create mode 100644 test/e2e/specs/interactivity/router-styles.spec.ts create mode 100644 test/integration/fixtures/blocks/core__button__deprecated-v12.html create mode 100644 test/integration/fixtures/blocks/core__button__deprecated-v12.json create mode 100644 test/integration/fixtures/blocks/core__button__deprecated-v12.parsed.json create mode 100644 test/integration/fixtures/blocks/core__button__deprecated-v12.serialized.html diff --git a/.eslintrc.js b/.eslintrc.js index d0c22090b93e87..e5f42eea656b90 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -137,6 +137,11 @@ const restrictedSyntax = [ message: 'Avoid truthy checks on length property rendering, as zero length is rendered verbatim.', }, + { + selector: + 'CallExpression[callee.name=/^(__|_x|_n|_nx)$/] > Literal[value=/^toggle\\b/i]', + message: "Avoid using the verb 'Toggle' in translatable strings", + }, ]; /** `no-restricted-syntax` rules for components. */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2ec03cba722c6b..3e02267da7c512 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -13,7 +13,7 @@ /packages/data-controls @nerrad # Blocks -/packages/block-library @ajitbohra +/packages/block-library @ajitbohra @fabiankaegy /packages/block-library/src/gallery @geriux /packages/block-library/src/comment-template @michalczaplinski /packages/block-library/src/comments @michalczaplinski diff --git a/.github/workflows/sync-assets-to-plugin-repo.yml b/.github/workflows/sync-assets-to-plugin-repo.yml new file mode 100644 index 00000000000000..c841b3ffc79579 --- /dev/null +++ b/.github/workflows/sync-assets-to-plugin-repo.yml @@ -0,0 +1,48 @@ +name: Sync Gutenberg plugin assets to WordPress.org plugin repo + +on: + push: + branches: + - trunk + paths: + - assets/** + +jobs: + sync-assets: + name: Sync assets to WordPress.org plugin repo + runs-on: ubuntu-latest + environment: wp.org plugin + env: + PLUGIN_REPO_URL: 'https://plugins.svn.wordpress.org/gutenberg' + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + + steps: + - name: Check out Gutenberg assets folder from WP.org plugin repo + run: | + svn checkout "$PLUGIN_REPO_URL/assets" \ + --username "$SVN_USERNAME" --password "$SVN_PASSWORD" + + - name: Delete everything + run: find assets -type f -not -path 'assets/.svn/*' -delete + + - name: Checkout assets from current release + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + sparse-checkout: | + assets + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + path: git + + - name: Copy files from git checkout to svn working copy + run: cp -R git/assets/* assets + + - name: Commit the updated assets + working-directory: ./assets + run: | + svn st | awk '/^?/ {print $2}' | xargs -r svn add + svn st | awk '/^!/ {print $2}' | xargs -r svn rm + svn commit . \ + -m "Sync assets for commit $GITHUB_SHA" \ + --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD" \ + --config-option=servers:global:http-timeout=600 diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index e866964e69b2d1..4d2b0a66a7e7d6 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -168,7 +168,9 @@ jobs: steps: - name: Check out Gutenberg trunk from WP.org plugin repo - run: svn checkout "$PLUGIN_REPO_URL/trunk" --username "$SVN_USERNAME" --password "$SVN_PASSWORD" + run: | + svn checkout "$PLUGIN_REPO_URL/trunk" --username "$SVN_USERNAME" --password "$SVN_PASSWORD" + svn checkout "$PLUGIN_REPO_URL/tags" --depth=immediates --username "$SVN_USERNAME" --password "$SVN_PASSWORD" - name: Delete everything working-directory: ./trunk @@ -182,7 +184,7 @@ jobs: unzip gutenberg.zip -d trunk rm gutenberg.zip - - name: Replace the stable tag placeholder with the existing stable tag on the SVN repository + - name: Replace the stable tag placeholder with the new version env: STABLE_TAG_PLACEHOLDER: 'Stable tag: V\.V\.V' run: | @@ -194,27 +196,16 @@ jobs: name: changelog trunk path: trunk - - name: Commit the content changes + - name: Commit the release working-directory: ./trunk run: | svn st | grep '^?' | awk '{print $2}' | xargs -r svn add svn st | grep '^!' | awk '{print $2}' | xargs -r svn rm - svn commit -m "Committing version $VERSION" \ + svn cp . "../tags/$VERSION" + svn commit . "../tags/$VERSION" \ + -m "Releasing version $VERSION" \ --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD" \ - --config-option=servers:global:http-timeout=300 - - - name: Create the SVN tag - working-directory: ./trunk - run: | - svn copy "$PLUGIN_REPO_URL/trunk" "$PLUGIN_REPO_URL/tags/$VERSION" -m "Tagging version $VERSION" \ - --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD" - - - name: Update the plugin's stable version - working-directory: ./trunk - run: | - sed -i "s/Stable tag: ${STABLE_VERSION_REGEX}/Stable tag: ${VERSION}/g" ./readme.txt - svn commit -m "Releasing version $VERSION" \ - --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD" + --config-option=servers:global:http-timeout=600 upload-tag: name: Publish as tag diff --git a/LICENSE.md b/LICENSE.md index 983294723c4806..12a05f0c071a68 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ ## Gutenberg - Copyright 2016-2024 by the contributors + Copyright 2016-2025 by the contributors **License for Contributions (on and after April 15, 2021)** diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 00000000000000..e437ec744d3807 --- /dev/null +++ b/assets/README.md @@ -0,0 +1,7 @@ +## Gutenberg Plugin Assets + +The contents of this directory are synced from the [`assets/` directory in the Gutenberg repository on GitHub](https://github.com/WordPress/gutenberg/tree/trunk/assets) to the [`assets/` directory of the Gutenberg WordPress.org plugin repository](https://plugins.trac.wordpress.org/browser/gutenberg/assets). **Any changes committed directly to the plugin repository on WordPress.org will be overwritten.** + +The sync is performed by a [GitHub Actions workflow](https://github.com/WordPress/gutenberg/actions/workflows/sync-assets-to-plugin-repo.yml) that is triggered whenever a file in this directory is changed. + +Since that workflow requires access to WP.org plugin repository credentials, it needs to be approved manually by a member of the Gutenberg Core team. If you don't have the necessary permissions, please ask someone in [#core-editor](https://wordpress.slack.com/archives/C02QB2JS7). diff --git a/assets/banner-1544x500.jpg b/assets/banner-1544x500.jpg new file mode 100644 index 0000000000000000000000000000000000000000..12e7192dd4285e13448ce2390abf51492caa06ba GIT binary patch literal 363052 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

9jsUU+cn#K*?k8V(AfA&uAp1YfyjqLaC?c> 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⋙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%0A7N3=L>N_l4dDeP$N}Lqf(6lR%5uY@B1n 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 Z5gl(ycb+XI5LEaFVEO;?HIiRb{|0*@aIOFV literal 0 HcmV?d00001 diff --git a/assets/banner-772x250.jpg b/assets/banner-772x250.jpg new file mode 100644 index 0000000000000000000000000000000000000000..316f7741071cbebcd8279c2534d56f4adcefa350 GIT binary patch literal 108852 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�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 T35`y!=6;MA?|N?MzuEr*PDXfs literal 0 HcmV?d00001 diff --git a/assets/icon-256x256.jpg b/assets/icon-256x256.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b7497f61652b7bd3874c314acdd1212c226be04f GIT binary patch literal 51683 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°-aiFsi)%e8;&2!p`YD=PbjrZ?7nVb$+i z*E09lhMu@0s;Xt#Vix5^GwWetmj$A3mAx&OEvV?X5Zn8oXYM z!o9g4*diZMZ3pNJOE!07(GHUrC#HOaKaII$GX*={o8Yd zxD1n4Cn3X>ETYo!Q{#|ebj^_6SORrtdT2_H1uX42agL)l9p?5SX$Hjfh&U8~=eVz- z#HfF$iIa}66oa=G``(IgStG+H?F6!Dg1(84pn`I!O(1=?hCwM_cIk4x)}jN03CW4# zVOn?%ArQDNpXgC(fYw$`+341Ch|S7SOsLigNK5aRqX_+e!K@V-mKChCuZb?H(yF_r zl4_wwkCGSgd$2S7x3UVyOaw=Et?K;V{f?=#PDKh@q|R}-n&MRdv{Biz%?>O7c>j5W zdkh`WN4_+yE?o&!AfPS=g|hY{Q{f);y1%@GWpIhJ@eq6!elrNuC&rg9U;L-qSC$7@ z(4|8!h8+bI+psOOiet!@)3&gHr=ZXp5+Pe2btdR@IEp)--f0Zjwn@V8lIBG=ZgI;_ zaa*7Cj`hlP>6FFDiv*{SrGS8dJjIbd!d{m4tOdzfY(4G8+c(s-+4O$P(>;~t4iH5< zoE94H8kjnSvASiK1xqjPHsBy z$RX`Zu}UGVr^%Pi&YWq!ljT+dtJz^o?40lzPK7OOeJgR80xdPt(W+CyjjYXxuuuG#p z08)Y7!G}kX43|o@?B7$GLTE`+15swSzj%>ZTyHL>m}SKqZI)C1gvr42v*ZnpLFtd1v&Omx9NF=MvJg;WmPdQ;S{f`ydy?|lN0H@O5m+7Mo}*dfHT)T zP)YkD#mJb8p|B=mq>x^v(A&RZk^a2ovBrBMp}IZRT6_1nIIZn!V5*(`Xcw*x#y1vK z#Gzp|So_Ft>96GqMxHU1W;7sN{(MNe>Fc1^ zG%6%}sPqEUx4Dd)tUR0sqw&kuWeGf|&IuSwzf94PdO98tix;6l7%0#}F{QAteh zbbKC_VWe};PXs|}oqoRexNg^~Jd9%DFXse$LlrXteIW(iP=uM2mDwS62vG2C1q0?v zqiLoqr9su^wyBP=;oFY>Ma{;MRI z3bXrXF`&BB^~KFRClmh{PZ>dGU;<)jVj}beoO{4x-o=^h%do)jj1#iJN*QsX^ib_K z5nFrdS=VK^-|{-}xML{uKinmAP4B)5Z`KlArO~85Kvp?pH?4t$qeg0S&6S`wuFIRK z>^l;DvN}Z!6uE{S6)iIoM#_xRI6o(2A!4g%9C?+jrTM7bV7y!-o_hSzzNKBJag7@F z&^(hJBUC~|53re>~flU`SHMfC*e>R|l(;-DPC2!1{cg0&*6uzG~bJ($buWPRL�>5t(5F> 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 R} Array of tags. + */ +export async function getTagsFromStorybook( filePath ) { + const fileContent = await fs.readFile( filePath, 'utf8' ); + const parsedFile = babel.parse( fileContent, { + filename: filePath, + } ); + + const meta = parsedFile.program.body.find( + ( node ) => + node.type === 'VariableDeclaration' && + node.declarations[ 0 ].id.name === 'meta' + ); + + return ( + meta.declarations[ 0 ].init.properties + .find( ( node ) => node.key.name === 'tags' ) + ?.value.elements.map( ( node ) => node.value ) ?? [] + ); +} diff --git a/bin/api-docs/gen-components-docs/index.mjs b/bin/api-docs/gen-components-docs/index.mjs index c7109dc4982c36..30888acf851cab 100644 --- a/bin/api-docs/gen-components-docs/index.mjs +++ b/bin/api-docs/gen-components-docs/index.mjs @@ -11,6 +11,7 @@ import path from 'path'; */ import { generateMarkdownDocs } from './markdown/index.mjs'; import { getDescriptionsForSubcomponents } from './get-subcomponent-descriptions.mjs'; +import { getTagsFromStorybook } from './get-tags-from-storybook.mjs'; const MANIFEST_GLOB = 'packages/components/src/**/docs-manifest.json'; @@ -113,9 +114,17 @@ await Promise.all( } ) ?? [] ); + const tags = await getTagsFromStorybook( + path.resolve( + path.dirname( manifestPath ), + 'stories/index.story.tsx' + ) + ); + const docs = generateMarkdownDocs( { typeDocs, subcomponentTypeDocs, + tags, } ); const outputFile = path.resolve( path.dirname( manifestPath ), diff --git a/bin/api-docs/gen-components-docs/markdown/index.mjs b/bin/api-docs/gen-components-docs/markdown/index.mjs index 126fdf0057b6e5..28e20dc3de12e0 100644 --- a/bin/api-docs/gen-components-docs/markdown/index.mjs +++ b/bin/api-docs/gen-components-docs/markdown/index.mjs @@ -8,14 +8,33 @@ import json2md from 'json2md'; */ import { generateMarkdownPropsJson } from './props.mjs'; -export function generateMarkdownDocs( { typeDocs, subcomponentTypeDocs } ) { +/** + * Converter for strings that are already formatted as Markdown. + * + * @param {string} [input] + * @return {string} The trimmed input if it is contentful, otherwise an empty string. + */ +json2md.converters.md = ( input ) => { + return input?.trim() || ''; +}; + +export function generateMarkdownDocs( { + typeDocs, + subcomponentTypeDocs, + tags, +} ) { const mainDocsJson = [ { h1: typeDocs.displayName }, '', + tags.includes( 'status-private' ) && [ + { + p: '🔒 This component is locked as a [private API](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-private-apis/). We do not yet recommend using this outside of the Gutenberg project.', + }, + ], { p: `

See the WordPress Storybook for more detailed, interactive documentation.

`, }, - typeDocs.description, + { md: typeDocs.description }, ...generateMarkdownPropsJson( typeDocs.props ), ]; @@ -26,7 +45,7 @@ export function generateMarkdownDocs( { typeDocs, subcomponentTypeDocs } ) { { h3: subcomponentTypeDoc.displayName, }, - subcomponentTypeDoc.description, + { md: subcomponentTypeDoc.description }, ...generateMarkdownPropsJson( subcomponentTypeDoc.props, { headingLevel: 4, } ), @@ -36,5 +55,5 @@ export function generateMarkdownDocs( { typeDocs, subcomponentTypeDocs } ) { return json2md( [ ...mainDocsJson, ...subcomponentDocsJson ].filter( Boolean ) - ); + ).replace( /\n+$/gm, '\n' ); // clean unnecessary consecutive newlines } diff --git a/bin/api-docs/gen-components-docs/markdown/props.mjs b/bin/api-docs/gen-components-docs/markdown/props.mjs index 9d019c4240f008..bacd86256f7e6a 100644 --- a/bin/api-docs/gen-components-docs/markdown/props.mjs +++ b/bin/api-docs/gen-components-docs/markdown/props.mjs @@ -33,7 +33,6 @@ export function generateMarkdownPropsJson( props, { headingLevel = 2 } = {} ) { return [ { [ `h${ headingLevel + 1 }` ]: `\`${ key }\`` }, - prop.description, { ul: [ `Type: \`${ renderPropType( prop.type ) }\``, @@ -42,10 +41,10 @@ export function generateMarkdownPropsJson( props, { headingLevel = 2 } = {} ) { `Default: \`${ prop.defaultValue.value }\``, ].filter( Boolean ), }, + { md: prop.description }, ]; } ) .filter( Boolean ); return [ { [ `h${ headingLevel }` ]: 'Props' }, ...propsJson ]; } - diff --git a/bin/check-licenses.mjs b/bin/check-licenses.mjs index 458590e696a9fd..b453ebd84cd3a7 100755 --- a/bin/check-licenses.mjs +++ b/bin/check-licenses.mjs @@ -10,7 +10,7 @@ import { spawnSync } from 'node:child_process'; */ import { checkDepsInTree } from '../packages/scripts/utils/license.js'; -const ignored = [ '@ampproject/remapping' ]; +const ignored = [ '@ampproject/remapping', 'webpack' ]; /* * `wp-scripts check-licenses` uses prod and dev dependencies of the package to scan for dependencies. With npm workspaces, workspace packages (the @wordpress/* packages) are not listed in the main package json and this approach does not work. diff --git a/bin/plugin/lib/utils.js b/bin/plugin/lib/utils.js index 4f57269d60c772..f4ef86c96ff081 100644 --- a/bin/plugin/lib/utils.js +++ b/bin/plugin/lib/utils.js @@ -2,7 +2,7 @@ * External dependencies */ // @ts-ignore -const inquirer = require( 'inquirer' ); +const { confirm } = require( '@inquirer/prompts' ); const fs = require( 'fs' ); const childProcess = require( 'child_process' ); const { v4: uuid } = require( 'uuid' ); @@ -97,14 +97,19 @@ async function askForConfirmation( isDefault = true, abortMessage = 'Aborting.' ) { - const { isReady } = await inquirer.prompt( [ - { - type: 'confirm', - name: 'isReady', + let isReady = false; + try { + isReady = await confirm( { default: isDefault, message, - }, - ] ); + } ); + } catch ( error ) { + if ( error instanceof Error && error.name === 'ExitPromptError' ) { + console.log( 'Cancelled.' ); + process.exit( 1 ); + } + throw error; + } if ( ! isReady ) { log( formats.error( '\n' + abortMessage ) ); diff --git a/bin/test-create-block.sh b/bin/test-create-block.sh index 99b7e8e6082604..7df3b214af042d 100755 --- a/bin/test-create-block.sh +++ b/bin/test-create-block.sh @@ -56,7 +56,7 @@ if [ "$expected" -ne "$actual" ]; then exit 1 fi expected=7 -actual=$( find src -maxdepth 1 -type f | wc -l ) +actual=$( find src -maxdepth 2 -type f | wc -l ) if [ "$expected" -ne "$actual" ]; then error "Expected $expected files in the \`src\` directory, but found $actual." exit 1 @@ -70,7 +70,7 @@ status "Building block..." status "Verifying build..." expected=9 -actual=$( find build -maxdepth 1 -type f | wc -l ) +actual=$( find build -maxdepth 2 -type f | wc -l ) if [ "$expected" -ne "$actual" ]; then error "Expected $expected files in the \`build\` directory, but found $actual." exit 1 diff --git a/bin/tsconfig.json b/bin/tsconfig.json index 3ec5d5826a045d..4baf899c9dce9e 100644 --- a/bin/tsconfig.json +++ b/bin/tsconfig.json @@ -16,6 +16,7 @@ "noEmit": true, "outDir": ".cache" }, + "include": [], "files": [ "./api-docs/update-api-docs.js", "./plugin/config.js", diff --git a/bin/validate-tsconfig.mjs b/bin/validate-tsconfig.mjs index 91d74b1bdb413f..47d6a320d7290e 100755 --- a/bin/validate-tsconfig.mjs +++ b/bin/validate-tsconfig.mjs @@ -29,14 +29,33 @@ for ( const packageName of packagesWithTypes ) { hasErrors = true; } - const packageJson = JSON.parse( - readFileSync( `packages/${ packageName }/package.json`, 'utf8' ) - ); - const tsconfigJson = JSON.parse( - stripJsonComments( - readFileSync( `packages/${ packageName }/tsconfig.json`, 'utf8' ) - ) - ); + let packageJson; + try { + packageJson = JSON.parse( + readFileSync( `packages/${ packageName }/package.json`, 'utf8' ) + ); + } catch ( e ) { + console.error( + `Error parsing package.json for package ${ packageName }` + ); + throw e; + } + let tsconfigJson; + try { + tsconfigJson = JSON.parse( + stripJsonComments( + readFileSync( + `packages/${ packageName }/tsconfig.json`, + 'utf8' + ) + ) + ); + } catch ( e ) { + console.error( + `Error parsing tsconfig.json for package ${ packageName }` + ); + throw e; + } if ( packageJson.dependencies ) { for ( const dependency of Object.keys( packageJson.dependencies ) ) { if ( dependency.startsWith( '@wordpress/' ) ) { diff --git a/changelog.txt b/changelog.txt index 38b33267554982..bb71ae8617d7f4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,845 @@ == Changelog == += 20.0.0-rc.1 = + + +## Changelog + +### Features + +#### Interactivity API +- Prevent each directive errors and allow any iterable. ([67798](https://github.com/WordPress/gutenberg/pull/67798)) + + +### Enhancements + +- Add dropdown menu props to ToolsPanel component. ([68019](https://github.com/WordPress/gutenberg/pull/68019)) +- Create Block: Allow external templates to customize more fields. ([68193](https://github.com/WordPress/gutenberg/pull/68193)) +- Create Block: Optimize the default template for multiple blocks case. ([68175](https://github.com/WordPress/gutenberg/pull/68175)) +- DOM: Support class wildcard matcher in 'cleanNodeList'. ([67830](https://github.com/WordPress/gutenberg/pull/67830)) +- Scripts: Recommend passing JS entry points with paths. ([68251](https://github.com/WordPress/gutenberg/pull/68251)) +- Upgrade sass to version 1.54.0. ([68380](https://github.com/WordPress/gutenberg/pull/68380)) +- Use Badge component in dataview grids. ([68062](https://github.com/WordPress/gutenberg/pull/68062)) +- Use Badge component in page markers. ([68103](https://github.com/WordPress/gutenberg/pull/68103)) +- postcss-plugins-preset: Bump autoprefixer to 10.4.20. ([68237](https://github.com/WordPress/gutenberg/pull/68237)) +- wp-env: Add multisite support. ([67845](https://github.com/WordPress/gutenberg/pull/67845)) + +#### Block Library +- Add Tools Panel dropdown menu props to More block. ([68039](https://github.com/WordPress/gutenberg/pull/68039)) +- Add block example attribute for Comments Form block. ([68267](https://github.com/WordPress/gutenberg/pull/68267)) +- Add block example attribute for Comments block. ([68266](https://github.com/WordPress/gutenberg/pull/68266)) +- Archive: Add dropdown menu props to ToolsPanel component. ([68010](https://github.com/WordPress/gutenberg/pull/68010)) +- Archives Block: Refactor setting panel. ([67841](https://github.com/WordPress/gutenberg/pull/67841)) +- Button Block: Refactor setting panel. ([67887](https://github.com/WordPress/gutenberg/pull/67887)) +- Button: Update Settings text labels. ([68265](https://github.com/WordPress/gutenberg/pull/68265)) +- Date Block: Add dropdown menu props to ToolsPanel component. ([68018](https://github.com/WordPress/gutenberg/pull/68018)) +- Date Block: Refactor settings panel to use ToolsPanel. ([67906](https://github.com/WordPress/gutenberg/pull/67906)) +- Details Block: Migrate to Toolspanel. ([67966](https://github.com/WordPress/gutenberg/pull/67966)) +- Excerpt Block: Refactor settings panel to use ToolsPanel. ([67908](https://github.com/WordPress/gutenberg/pull/67908)) +- Featured Image Block: Refactor setting panel. ([67456](https://github.com/WordPress/gutenberg/pull/67456)) +- Introduce new filter "render_block_core_navigation_link_allowed_post_status". ([63181](https://github.com/WordPress/gutenberg/pull/63181)) +- Latest Posts Border Block Support. ([66353](https://github.com/WordPress/gutenberg/pull/66353)) +- Login/Logout: Add dropdown menu props to ToolsPanel component. ([68009](https://github.com/WordPress/gutenberg/pull/68009)) +- Login/Logout: Refactor settings panel to use ToolsPanel. ([67909](https://github.com/WordPress/gutenberg/pull/67909)) +- More Block: Refactor settings panel to use ToolsPanel. ([67905](https://github.com/WordPress/gutenberg/pull/67905)) +- Navigation Submenu Block: Refactor settings panel to use ToolsPanel. ([67969](https://github.com/WordPress/gutenberg/pull/67969)) +- Page List Block: Add dropdown menu props to ToolsPanel component. ([68012](https://github.com/WordPress/gutenberg/pull/68012)) +- Page List Block: Refactor settings panel to use ToolsPanel. ([67903](https://github.com/WordPress/gutenberg/pull/67903)) +- Post Featured Image: Use the 'ResolutionTool' component. ([68294](https://github.com/WordPress/gutenberg/pull/68294)) +- Query Page Numbers Block: Refactor settings panel to use ToolsPanel. ([67958](https://github.com/WordPress/gutenberg/pull/67958)) +- Query Page Numbers: Add dropdown menu props to ToolsPanel component. ([68013](https://github.com/WordPress/gutenberg/pull/68013)) +- Query Pagination: Refactor settings panel to use ToolsPanel. ([67914](https://github.com/WordPress/gutenberg/pull/67914)) +- Query Pagination: Update 'showLabel' help text. ([68105](https://github.com/WordPress/gutenberg/pull/68105)) +- Query Total block: Reduce concatenation in the output text. ([68150](https://github.com/WordPress/gutenberg/pull/68150)) +- Read More: Add example preview. ([68288](https://github.com/WordPress/gutenberg/pull/68288)) +- Refactor "Settings" panel of Navigation Item block to use ToolsPanel instead of PanelBody. ([67973](https://github.com/WordPress/gutenberg/pull/67973)) +- Replace PanelBody with ToolsPanel and ToolsPanelItem in column block. ([67913](https://github.com/WordPress/gutenberg/pull/67913)) +- Replace PanelBody with ToolsPanel and ToolsPanelItem in spacer block. ([67981](https://github.com/WordPress/gutenberg/pull/67981)) +- Replace PanelBody with ToolsPanel in columns block. ([67910](https://github.com/WordPress/gutenberg/pull/67910)) +- Site Title Block: Add dropdown menu props to ToolsPanel component. ([68017](https://github.com/WordPress/gutenberg/pull/68017)) +- Site Title Block: Refactor settings panel to use ToolsPanel. ([67898](https://github.com/WordPress/gutenberg/pull/67898)) +- Social Icon: Migrate to Toolspanel. ([67974](https://github.com/WordPress/gutenberg/pull/67974)) +- Social Icons: Migrate to Toolspanel. ([67975](https://github.com/WordPress/gutenberg/pull/67975)) +- Table Block: Refactor settings panel to use ToolsPanel. ([67896](https://github.com/WordPress/gutenberg/pull/67896)) +- Tag Cloud Block: Refactor settings panel to use ToolsPanel. ([67911](https://github.com/WordPress/gutenberg/pull/67911)) +- Video Block: Refactor setting panel. ([67044](https://github.com/WordPress/gutenberg/pull/67044)) + +#### Components +- : Badge Component. ([66555](https://github.com/WordPress/gutenberg/pull/66555)) +- Badge: Support text truncation. ([68107](https://github.com/WordPress/gutenberg/pull/68107)) +- Button: Add hover style to `secondary` variant. ([67325](https://github.com/WordPress/gutenberg/pull/67325)) +- CreateTemplatePartModalContents: Use native radio inputs. ([67702](https://github.com/WordPress/gutenberg/pull/67702)) +- Menu: More granular sub-components. ([67422](https://github.com/WordPress/gutenberg/pull/67422)) +- RangeControl: Animate thumb and track only when using marks. ([67836](https://github.com/WordPress/gutenberg/pull/67836)) +- Storybook: Add more `max-width` containers. ([68080](https://github.com/WordPress/gutenberg/pull/68080)) +- Storybook: Upgrade to the latest version (v8.4.7). ([67863](https://github.com/WordPress/gutenberg/pull/67863)) +- Storybook: Upgrade to v8.0.x. ([67574](https://github.com/WordPress/gutenberg/pull/67574)) +- Unite inline Ariakit imports. ([67818](https://github.com/WordPress/gutenberg/pull/67818)) + +#### Style Book +- Give style book its own route so it can be linked to directly. ([67811](https://github.com/WordPress/gutenberg/pull/67811)) +- Stylebook: Add the Appearance -> Design submenu through `admin_menu` action. ([68174](https://github.com/WordPress/gutenberg/pull/68174)) +- Try splitting style book into sections. ([68071](https://github.com/WordPress/gutenberg/pull/68071)) +- Try toggle instead of dropdown to show stylebook. ([67810](https://github.com/WordPress/gutenberg/pull/67810)) + +#### Design Tools +- Post Comments Link: Add Border Support. ([68450](https://github.com/WordPress/gutenberg/pull/68450)) +- Post Template: Add Border and Spacing Support. ([64425](https://github.com/WordPress/gutenberg/pull/64425)) +- Query Total: Add Border Support. ([68323](https://github.com/WordPress/gutenberg/pull/68323)) + +#### Block Editor +- Add reset button to ColorGradientSettingsDropdown. ([67800](https://github.com/WordPress/gutenberg/pull/67800)) +- ChildLayoutControl: Use units defined in theme.json. ([67784](https://github.com/WordPress/gutenberg/pull/67784)) +- KeyboardShortcuts: Update delete shortcut to use `shift + Backspace`. ([68164](https://github.com/WordPress/gutenberg/pull/68164)) + +#### Block hooks +- Apply to Post Content (on frontend and in editor). ([67272](https://github.com/WordPress/gutenberg/pull/67272)) +- Synced Patterns: Apply Block Hooks. ([68058](https://github.com/WordPress/gutenberg/pull/68058)) + +#### Media +- Split upload into verbs and nouns. ([68227](https://github.com/WordPress/gutenberg/pull/68227)) + +#### Zoom Out +- Remove placeholder of default paragraph when it's the only block and canvas is zoomed out. ([68106](https://github.com/WordPress/gutenberg/pull/68106)) + +#### Interactivity API +- iAPI Router: Handle styles assets on region-based navigation. ([67826](https://github.com/WordPress/gutenberg/pull/67826)) + +#### Plugin +- Add a Playground blueprint json to the /assets/blueprints folder of Plugin Repo. ([67742](https://github.com/WordPress/gutenberg/pull/67742)) + +#### Site Editor +- Pages: Add "Set as posts page" action. ([67650](https://github.com/WordPress/gutenberg/pull/67650)) + +#### Write mode +- Allow template part editing in write mode. ([67372](https://github.com/WordPress/gutenberg/pull/67372)) + +#### Patterns +- Replace Starter Content modal with inserter panel. ([66836](https://github.com/WordPress/gutenberg/pull/66836)) + +#### Commands +- Add command to navigate to site editor. ([66722](https://github.com/WordPress/gutenberg/pull/66722)) + +#### Inspector Controls +- Use custom name in block sidebar if available (retaining block type information). ([65641](https://github.com/WordPress/gutenberg/pull/65641)) + + +### New APIs + +#### Components +- BoxControl: Add support for presets. ([67688](https://github.com/WordPress/gutenberg/pull/67688)) + + +### Bug Fixes + +- Add duotone and dimensions to the block level for translation. ([68243](https://github.com/WordPress/gutenberg/pull/68243)) +- Add text domain option while scaffolding the block in create-block. ([57197](https://github.com/WordPress/gutenberg/pull/57197)) +- Added `is-focus-mode` class on all viewports. ([67377](https://github.com/WordPress/gutenberg/pull/67377)) +- Editor: Fix initial edits applied again after saving the post. ([68273](https://github.com/WordPress/gutenberg/pull/68273)) +- Fix dataviews commonjs export. ([67962](https://github.com/WordPress/gutenberg/pull/67962)) +- Get active element within the iframe when restoring focus. ([68060](https://github.com/WordPress/gutenberg/pull/68060)) +- Make strings in theme.json translatable. ([66675](https://github.com/WordPress/gutenberg/pull/66675)) +- Scripts: Use fork of `rtlcss-webpack-plugin` to fix issues with deps. ([68201](https://github.com/WordPress/gutenberg/pull/68201)) + +#### Block Library +- Columns: Add space above notice text. ([68259](https://github.com/WordPress/gutenberg/pull/68259)) +- Enhance: Improve pagination logic in core/query-pagination-previous block. ([68070](https://github.com/WordPress/gutenberg/pull/68070)) +- Fix author information leakage by author blocks for Custom Post Types without author support & display notice to user. ([67136](https://github.com/WordPress/gutenberg/pull/67136)) +- Media & Text: Correctly reset the 'useFeaturedImage' attribute. ([68247](https://github.com/WordPress/gutenberg/pull/68247)) +- Navigation Submenu Block: Add dropdown menu props to ToolsPanel component. ([68015](https://github.com/WordPress/gutenberg/pull/68015)) +- Page List Block: Fix critical error when converting to link. ([68076](https://github.com/WordPress/gutenberg/pull/68076)) +- Page List block: Don't wrap Edit button with ToolsPanelItem component. ([68248](https://github.com/WordPress/gutenberg/pull/68248)) +- Query Total: Remove nested element. ([68304](https://github.com/WordPress/gutenberg/pull/68304)) +- Table Block: Fix margin/padding to include caption in spacing. ([68281](https://github.com/WordPress/gutenberg/pull/68281)) +- Update SiteTitle block to Fix `isLink` Toggle Behavior. ([68295](https://github.com/WordPress/gutenberg/pull/68295)) +- i18n: Make example and variations translatable in `post-navigation-link`. ([68375](https://github.com/WordPress/gutenberg/pull/68375)) +- i18n: Make example translatable in `query-no-results`. ([68376](https://github.com/WordPress/gutenberg/pull/68376)) +- i18n: Make example translatable in `table-of-contents`. ([68377](https://github.com/WordPress/gutenberg/pull/68377)) + +#### Components +- Block Editor: Fix the 'Reset all' bug for the 'ResolutionTool' component. ([68296](https://github.com/WordPress/gutenberg/pull/68296)) +- BoxControl: Better minimum value support. ([67819](https://github.com/WordPress/gutenberg/pull/67819)) +- BoxControl: Fix `aria-valuetext` value. ([68362](https://github.com/WordPress/gutenberg/pull/68362)) +- Fix end-to-end storybook. ([68307](https://github.com/WordPress/gutenberg/pull/68307)) +- Fixing Text Contrast for Dark Mode. ([68349](https://github.com/WordPress/gutenberg/pull/68349)) +- FontSizePicker: Add `display: Contents` rule to custom size select. ([68280](https://github.com/WordPress/gutenberg/pull/68280)) +- Storybook: Fix `emotion/is-prop-valid` warning. ([68202](https://github.com/WordPress/gutenberg/pull/68202)) +- Storybook: Fix a few editor styles warnings. ([68198](https://github.com/WordPress/gutenberg/pull/68198)) +- Storybook: Fix warnings in Layout document. ([67865](https://github.com/WordPress/gutenberg/pull/67865)) +- Use default value in `useMediaUploadSettings`. ([68100](https://github.com/WordPress/gutenberg/pull/68100)) + +#### Block Editor +- Media Replace Flow: Add custom toggle support and fix button height. ([68084](https://github.com/WordPress/gutenberg/pull/68084)) +- BlockCard: Fix title alignment. ([68115](https://github.com/WordPress/gutenberg/pull/68115)) +- DateFormatPicker: Fix styles & spacing. ([68079](https://github.com/WordPress/gutenberg/pull/68079)) +- Fix Iframe error for links without 'href'. ([68024](https://github.com/WordPress/gutenberg/pull/68024)) +- Grid Visualizer: Improve observation logic. ([68230](https://github.com/WordPress/gutenberg/pull/68230)) +- List View: Fix appender size. ([68221](https://github.com/WordPress/gutenberg/pull/68221)) +- MediaReplaceFlow: Remove store subscription in favor of modern CSS. ([68276](https://github.com/WordPress/gutenberg/pull/68276)) +- Remove patterns from the Quick Inserter to prevent misuse in block-specific contexts. ([67738](https://github.com/WordPress/gutenberg/pull/67738)) +- Revert 'Warning' component autofocus. ([68133](https://github.com/WordPress/gutenberg/pull/68133)) + +#### Post Editor +- DataViews: Fix text in action for setting site home page. ([67787](https://github.com/WordPress/gutenberg/pull/67787)) +- Edit post: Fix meta box pane’s pointer capture. ([68252](https://github.com/WordPress/gutenberg/pull/68252)) +- Editor: Remove HTML from the post title in the document bar. ([68358](https://github.com/WordPress/gutenberg/pull/68358)) +- Fix: Some 403 errors for editor roles. ([68146](https://github.com/WordPress/gutenberg/pull/68146)) +- Improve logic to show entities saved panel description. ([67971](https://github.com/WordPress/gutenberg/pull/67971)) + +#### DataViews +- Don't render actions dropdown when all eligible ones are `primary`. ([68168](https://github.com/WordPress/gutenberg/pull/68168)) +- Handle `grid` preview size based on container width. ([68078](https://github.com/WordPress/gutenberg/pull/68078)) +- Hide actions related UI in `grid` when no actions or bulk actions are passed. ([68033](https://github.com/WordPress/gutenberg/pull/68033)) +- Pages: Update layout-specific configuration when the view is updated. ([67881](https://github.com/WordPress/gutenberg/pull/67881)) +- Use `action.disabled` state to disable actions (primary and secondary). ([68275](https://github.com/WordPress/gutenberg/pull/68275)) + +#### Site Editor +- Add CSS classname to fix the negative margins not appearing in the Navigation Screen. ([67825](https://github.com/WordPress/gutenberg/pull/67825)) +- Fix obsolete `getLocationWithParams` usage. ([68388](https://github.com/WordPress/gutenberg/pull/68388)) +- Pages: Remove unnecessary padding for items. ([67977](https://github.com/WordPress/gutenberg/pull/67977)) +- Update active menu item appearance. ([68147](https://github.com/WordPress/gutenberg/pull/68147)) + +#### Style Book +- Fix global styles updating in style book. ([68111](https://github.com/WordPress/gutenberg/pull/68111)) +- Fix style book background color. ([68088](https://github.com/WordPress/gutenberg/pull/68088)) +- Fix uploading background images in stylebook view. ([68159](https://github.com/WordPress/gutenberg/pull/68159)) +- Stylebook: Avoid double line in subcategory titles. ([67752](https://github.com/WordPress/gutenberg/pull/67752)) + +#### Zoom Out +- Allow replace operation on empty default block in Zoom Out. ([68026](https://github.com/WordPress/gutenberg/pull/68026)) +- Fix don't show inserter in Zoom Out dropzone when the text is visible. ([68031](https://github.com/WordPress/gutenberg/pull/68031)) +- Hide separators for currently dragged section in Zoom Out. ([67638](https://github.com/WordPress/gutenberg/pull/67638)) +- Make Write mode and Zoom out block options menus consistent. ([67749](https://github.com/WordPress/gutenberg/pull/67749)) + +#### Design Tools +- Background supports: Add default controls supports. ([68085](https://github.com/WordPress/gutenberg/pull/68085)) +- Block supports: Show selected item in font family select control. ([68254](https://github.com/WordPress/gutenberg/pull/68254)) +- Fix: Ensure consistency in editor tools for navigation buttons and delete options. ([67253](https://github.com/WordPress/gutenberg/pull/67253)) + +#### Template Editor +- Fix: Editing "Page" is broken for low capability users. ([68110](https://github.com/WordPress/gutenberg/pull/68110)) +- Plugin: Fix eligibility check for post types' default rendering mode. ([67879](https://github.com/WordPress/gutenberg/pull/67879)) + +#### Widgets Editor +- Customizer Widgets: Fix inserter button size and animation. ([67880](https://github.com/WordPress/gutenberg/pull/67880)) +- Widget Editor: Fix: Close button is not working. ([65443](https://github.com/WordPress/gutenberg/pull/65443)) + +#### Meta Boxes +- Show metabox when pattern is accessed directly. ([68255](https://github.com/WordPress/gutenberg/pull/68255)) + +#### Typography +- Button Block: Set proper typography for inner elements. ([68023](https://github.com/WordPress/gutenberg/pull/68023)) + +#### History +- Query Pagination: Fix 'undo' trap. ([68022](https://github.com/WordPress/gutenberg/pull/68022)) + +#### npm Packages +- Add --glob argument to rimraf cli scripts. ([67829](https://github.com/WordPress/gutenberg/pull/67829)) + +#### Paste +- Image: Avoid link class loss when pasting for raw transformation. ([67803](https://github.com/WordPress/gutenberg/pull/67803)) + +#### Extensibility +- Make Block Bindings work with `editor.BlockEdit` hook (2nd try). ([67523](https://github.com/WordPress/gutenberg/pull/67523)) + + +### Accessibility + +- Dataviews List layout: Do not use grid role on a `ul` element. ([67849](https://github.com/WordPress/gutenberg/pull/67849)) +- Fix: Templates and patterns are nesting two elements with the button role. ([67801](https://github.com/WordPress/gutenberg/pull/67801)) +- [Dataviews] Fix: Media item focus style is not visible on Grid. ([67789](https://github.com/WordPress/gutenberg/pull/67789)) + +#### Block Editor +- Fix: Inserter category tabs: Avoid unnecessary aria-label. ([68160](https://github.com/WordPress/gutenberg/pull/68160)) +- Improve accessibility of the Warning component in the block editor. ([67433](https://github.com/WordPress/gutenberg/pull/67433)) + +#### Global Styles +- Shadows: Always show reset button if hover is not supported. ([68122](https://github.com/WordPress/gutenberg/pull/68122)) +- Visual Refactor: Add Chevron Icon for Shadows in Global Styles. ([67720](https://github.com/WordPress/gutenberg/pull/67720)) + +#### Block Library +- Button: Replace ButtonGroup usage with ToggleGroupControl. ([65346](https://github.com/WordPress/gutenberg/pull/65346)) +- Fix Choose menu label when a menu has been deleted. ([67009](https://github.com/WordPress/gutenberg/pull/67009)) + +#### DataViews +- Add confirm dialog before Permanently delete. ([67824](https://github.com/WordPress/gutenberg/pull/67824)) + +#### Site Editor +- Make sure the sidebar navigation item focus style is fully visible. ([67817](https://github.com/WordPress/gutenberg/pull/67817)) + +#### Components +- CustomSelectControl: Refactor to use Ariakit store state for current value. ([67815](https://github.com/WordPress/gutenberg/pull/67815)) + + +### Performance + +#### Block Library +- Don't fetch media details if the block doesn't use a featured image. ([68299](https://github.com/WordPress/gutenberg/pull/68299)) +- Media & Text: Optimize block editor store subscriptions. ([68290](https://github.com/WordPress/gutenberg/pull/68290)) + + +### Experiments + +#### DataViews +- Proof of concept: Visualize hierarchical data. ([66479](https://github.com/WordPress/gutenberg/pull/66479)) + + +### Documentation + +- .wp-env.json schema: Add `testsPort` field. ([68220](https://github.com/WordPress/gutenberg/pull/68220)) +- Add README for TextAlignmentControl component. ([68126](https://github.com/WordPress/gutenberg/pull/68126)) +- Add layout related updates to the DataForm README. ([68050](https://github.com/WordPress/gutenberg/pull/68050)) +- Added Global Documentation in load.php. ([68325](https://github.com/WordPress/gutenberg/pull/68325)) +- Badge component: Fix Storybook URL link. ([68077](https://github.com/WordPress/gutenberg/pull/68077)) +- Badge: Fix up extra newline in readme. ([68359](https://github.com/WordPress/gutenberg/pull/68359)) +- Block Editor Storybook: Restructure the directory and add badges to private components. ([68352](https://github.com/WordPress/gutenberg/pull/68352)) +- Clarify template property behavior in InnerBlocks documentation to specify prefill when empty. ([66911](https://github.com/WordPress/gutenberg/pull/66911)) +- Components: Normalize newlines in auto-generated READMEs. ([68208](https://github.com/WordPress/gutenberg/pull/68208)) +- Components: Prevent broken lists in auto-generated readmes. ([68301](https://github.com/WordPress/gutenberg/pull/68301)) +- Components: Warn private API in auto-generated readmes. ([68317](https://github.com/WordPress/gutenberg/pull/68317)) +- Create a catalog list of private APIs. ([66558](https://github.com/WordPress/gutenberg/pull/66558)) +- DateFormatPicker: Improve line breaks in JSDoc and README. ([68006](https://github.com/WordPress/gutenberg/pull/68006)) +- Doc: Add JSDoc and update README for BlockCard component. ([68114](https://github.com/WordPress/gutenberg/pull/68114)) +- Docs: Fix some typos on reference-guide data-core-block-editor.md. ([68066](https://github.com/WordPress/gutenberg/pull/68066)) +- Documenting innerBlocks in save function. ([66689](https://github.com/WordPress/gutenberg/pull/66689)) +- Fix reference to `wp-env start` in documentation. ([68034](https://github.com/WordPress/gutenberg/pull/68034)) +- Fix wrong `npm start` command. ([65221](https://github.com/WordPress/gutenberg/pull/65221)) +- Fix: Fix link to minimal-block example plugin code. ([67888](https://github.com/WordPress/gutenberg/pull/67888)) +- Fixed typo in README of TextTransformControl. ([68443](https://github.com/WordPress/gutenberg/pull/68443)) +- Section Styles: Update block style variation documentation. ([68169](https://github.com/WordPress/gutenberg/pull/68169)) +- Storybook : Add TextTransformControl stories. ([67365](https://github.com/WordPress/gutenberg/pull/67365)) +- Storybook: Add BorderRadiusControl story. ([67383](https://github.com/WordPress/gutenberg/pull/67383)) +- Storybook: Add PlainText Storybook stories. ([67341](https://github.com/WordPress/gutenberg/pull/67341)) +- Storybook: Add stories for BlockCard component. ([67191](https://github.com/WordPress/gutenberg/pull/67191)) +- Storybook: Add stories for BlockTitle Component. ([67234](https://github.com/WordPress/gutenberg/pull/67234)) +- Storybook: Add stories for DateFormatPicker Component. ([67290](https://github.com/WordPress/gutenberg/pull/67290)) +- Storybook: Add stories for the ContrastChecker component. ([68120](https://github.com/WordPress/gutenberg/pull/68120)) +- Storybook: Add stories for the TextAlignmentControl component. ([67371](https://github.com/WordPress/gutenberg/pull/67371)) +- Storybook: Add stories for the TextDecorationControl component. ([67337](https://github.com/WordPress/gutenberg/pull/67337)) +- Storybook: Add story for the Warning component. ([68124](https://github.com/WordPress/gutenberg/pull/68124)) +- Storybook: Make prop sort order consistent. ([68152](https://github.com/WordPress/gutenberg/pull/68152)) +- Tabs: Auto-generate README. ([68209](https://github.com/WordPress/gutenberg/pull/68209)) +- Update platform documentation intro. ([61341](https://github.com/WordPress/gutenberg/pull/61341)) +- Update the copyright license to 2025. ([68440](https://github.com/WordPress/gutenberg/pull/68440)) +- Updated @since Doc Order in Inline documentation. ([68003](https://github.com/WordPress/gutenberg/pull/68003)) +- Updated Document URL in Documentation. ([67990](https://github.com/WordPress/gutenberg/pull/67990)) +- Updated Small Typo in documentation in docs/getting-started/faq.md file. ([68357](https://github.com/WordPress/gutenberg/pull/68357)) +- [Docs] Fix: Two broken links to the packages reference API and to blocks documentation. ([67889](https://github.com/WordPress/gutenberg/pull/67889)) +- env: Fix changelog entry. ([68219](https://github.com/WordPress/gutenberg/pull/68219)) +- theme.json schema: Fix block list. ([68343](https://github.com/WordPress/gutenberg/pull/68343)) + + +### Code Quality + +- Adding myself as a code owner of the block library package. ([67891](https://github.com/WordPress/gutenberg/pull/67891)) +- Create Block: Migrate Inquirer.js dependency to the new API. ([67877](https://github.com/WordPress/gutenberg/pull/67877)) +- Fix indentation in the upload-media tsconfig. ([68083](https://github.com/WordPress/gutenberg/pull/68083)) +- Fix indentation in upload-media package.json. ([68037](https://github.com/WordPress/gutenberg/pull/68037)) +- Fix: Invalid JSDoc syntax for optional object. ([68061](https://github.com/WordPress/gutenberg/pull/68061)) +- Remove some obsolete stylelint `at-rule-no-unknown` disable rules. ([68087](https://github.com/WordPress/gutenberg/pull/68087)) + +#### Components +- DatePicker: Prepare day buttons for 40px default size. ([68156](https://github.com/WordPress/gutenberg/pull/68156)) +- DropZone: Make the drop zone in Storybook the same size as the item. ([68231](https://github.com/WordPress/gutenberg/pull/68231)) +- Fix Button size violations in misc. unit tests. ([68154](https://github.com/WordPress/gutenberg/pull/68154)) +- Fix: Add soft deperecation notice for the ButtonGroup component. ([65429](https://github.com/WordPress/gutenberg/pull/65429)) +- InputControl : Deprecate 36px default size. ([66897](https://github.com/WordPress/gutenberg/pull/66897)) +- Menu: Migrate Storybook examples to CSF3. ([68204](https://github.com/WordPress/gutenberg/pull/68204)) +- Menu: Use ariakit types. ([68206](https://github.com/WordPress/gutenberg/pull/68206)) +- Navigation: Prepare for hard deprecation. ([68158](https://github.com/WordPress/gutenberg/pull/68158)) +- Navigation: Upsize back buttons. ([68157](https://github.com/WordPress/gutenberg/pull/68157)) +- RadioGroup: Log deprecation warning. ([68067](https://github.com/WordPress/gutenberg/pull/68067)) +- SelectControl : Deprecate 36px default size. ([66898](https://github.com/WordPress/gutenberg/pull/66898)) +- Slot: Use layout effect and update Cover block unit tests. ([68176](https://github.com/WordPress/gutenberg/pull/68176)) +- SlotFill: Use observableMap everywhere, remove manual rerendering. ([67400](https://github.com/WordPress/gutenberg/pull/67400)) +- Tabs: Use correct ariakit type for root component. ([68207](https://github.com/WordPress/gutenberg/pull/68207)) +- TreeSelect: Deprecate 36px default size. ([67855](https://github.com/WordPress/gutenberg/pull/67855)) + +#### Plugin +- chore: fix return type for `WP_Duotone_Gutenberg::Get_selector()`. ([66695](https://github.com/WordPress/gutenberg/pull/66695)) +- fix: Deprecated `WP_Webfonts()` constructor takes no arguments. ([66700](https://github.com/WordPress/gutenberg/pull/66700)) +- fix: Remove extraneous arg from `gutenberg_url()` call in `gutenberg_posts_dashboard()`. ([66699](https://github.com/WordPress/gutenberg/pull/66699)) +- fix: Remove extraneous param from `remove_filter()` calls. ([66697](https://github.com/WordPress/gutenberg/pull/66697)) +- fix: Wrong number of `$accepted_args` on `add_filter()` calls. ([66694](https://github.com/WordPress/gutenberg/pull/66694)) +- fix: explicitly return false in `WP_Theme_JSON_Gutenberg::Should_override_preset()`. ([66696](https://github.com/WordPress/gutenberg/pull/66696)) + +#### Block Editor +- Fix ESLint warnings for the 'useInnerBlockTemplateSync' hook. ([68355](https://github.com/WordPress/gutenberg/pull/68355)) +- FontAppearanceControl: Deprecate 36px default size. ([67854](https://github.com/WordPress/gutenberg/pull/67854)) +- FontFamilyControl: Deprecate 36px default size. ([67853](https://github.com/WordPress/gutenberg/pull/67853)) +- Inserter: Use 40px default size for toggle button. ([68155](https://github.com/WordPress/gutenberg/pull/68155)) +- LineHeightControl: Deprecate 36px default size. ([67850](https://github.com/WordPress/gutenberg/pull/67850)) + +#### Post Editor +- DocumentTools: Use standard ToolbarButton for inserter. ([68332](https://github.com/WordPress/gutenberg/pull/68332)) +- Editor: Remove constants for notices. ([68361](https://github.com/WordPress/gutenberg/pull/68361)) +- Editor: Remove the 'content-only' check from 'TemplatePartConverterMenuItem'. ([67961](https://github.com/WordPress/gutenberg/pull/67961)) + +#### DataViews +- DataForm: Add unit tests. ([68054](https://github.com/WordPress/gutenberg/pull/68054)) +- DataForm: Remove `FormFieldVisibility`. ([68203](https://github.com/WordPress/gutenberg/pull/68203)) +- [DataView] Initial list of unit tests for the DataView component. ([68205](https://github.com/WordPress/gutenberg/pull/68205)) + +#### Block Library +- Columns: Replace some store selectors with 'getBlockOrder'. ([67991](https://github.com/WordPress/gutenberg/pull/67991)) +- Fix trailing spaces in navigation block classnames. ([68161](https://github.com/WordPress/gutenberg/pull/68161)) + +#### Site Editor +- Edit Site: Standardize reduced motion handling using media queries. ([68419](https://github.com/WordPress/gutenberg/pull/68419)) + +#### Design Tools +- Block Supports: Revert stabilization of typography, border, skip serialization and default controls supports. ([68163](https://github.com/WordPress/gutenberg/pull/68163)) + +#### Zoom Out +- Correct spelling in Zoom Out Inserters comment. ([68051](https://github.com/WordPress/gutenberg/pull/68051)) + +#### Block API +- Fail gracefully when block in `createBlock` function is not registered. ([68043](https://github.com/WordPress/gutenberg/pull/68043)) + +#### Icons +- Deprecate `warning` and rename to `cautionFilled`. ([67895](https://github.com/WordPress/gutenberg/pull/67895)) + + +### Tools + +#### Build Tooling +- Add new private `upload-media` package. ([66290](https://github.com/WordPress/gutenberg/pull/66290)) +- Build: Simplify tsconfig.json files. ([68326](https://github.com/WordPress/gutenberg/pull/68326)) +- Clean script: Use braces instead of @-pattern for glob. ([67833](https://github.com/WordPress/gutenberg/pull/67833)) +- Fix VS Code performance. ([68347](https://github.com/WordPress/gutenberg/pull/68347)) +- Fix tsconfig for test/ directory. ([68346](https://github.com/WordPress/gutenberg/pull/68346)) +- Fix: Script with glob option doesn't work on Windows. ([67862](https://github.com/WordPress/gutenberg/pull/67862)) + +#### Testing +- Page - Quick Edit: Add end-to-end tests. ([68151](https://github.com/WordPress/gutenberg/pull/68151)) +- Add ESLint rule to prevent usage of the verb 'toggle' in translatable strings. ([67741](https://github.com/WordPress/gutenberg/pull/67741)) +- Enhance template registration end-to-end tests to handle welcome dialog visibility. ([68059](https://github.com/WordPress/gutenberg/pull/68059)) + + +### Various + +- ActionItem.Slot: Render as `MenuGroup` by default. ([67985](https://github.com/WordPress/gutenberg/pull/67985)) +- Storybook: Add BlockAlignmentMatrixControl Stories and update README. ([68007](https://github.com/WordPress/gutenberg/pull/68007)) +- Update "Call to Action" to "Call to action". ([67876](https://github.com/WordPress/gutenberg/pull/67876)) + +#### Plugin +- Assets: Add README.md about syncing. ([68128](https://github.com/WordPress/gutenberg/pull/68128)) +- Workflows: Sync assets to plugin repo upon change in trunk. ([68052](https://github.com/WordPress/gutenberg/pull/68052)) + + +## First-time contributors + +The following PRs were merged by first-time contributors: + +- @benazeer-ben: Add command to navigate to site editor. ([66722](https://github.com/WordPress/gutenberg/pull/66722)) +- @dhruvikpatel18: Fixed typo in README of TextTransformControl. ([68443](https://github.com/WordPress/gutenberg/pull/68443)) +- @fushar: Stylebook: Add the Appearance -> Design submenu through `admin_menu` action. ([68174](https://github.com/WordPress/gutenberg/pull/68174)) +- @im3dabasia: Storybook : Add TextTransformControl stories. ([67365](https://github.com/WordPress/gutenberg/pull/67365)) +- @justlevine: fix: Deprecated `WP_Webfonts()` constructor takes no arguments. ([66700](https://github.com/WordPress/gutenberg/pull/66700)) +- @karthick-murugan: Latest Posts Border Block Support. ([66353](https://github.com/WordPress/gutenberg/pull/66353)) +- @mayurprajapatii: Updated Document URL in Documentation. ([67990](https://github.com/WordPress/gutenberg/pull/67990)) +- @PARTHVATALIYA: Widget Editor: Fix: Close button is not working. ([65443](https://github.com/WordPress/gutenberg/pull/65443)) +- @prasadkarmalkar: Replace PanelBody with ToolsPanel and ToolsPanelItem in column block. ([67913](https://github.com/WordPress/gutenberg/pull/67913)) +- @rilwis: Fix wrong `npm start` command. ([65221](https://github.com/WordPress/gutenberg/pull/65221)) +- @sarthaknagoshe2002: Clarify template property behavior in InnerBlocks documentation to specify prefill when empty. ([66911](https://github.com/WordPress/gutenberg/pull/66911)) +- @timse201: Split upload into verbs and nouns. ([68227](https://github.com/WordPress/gutenberg/pull/68227)) +- @vampdroid: Add text domain option while scaffolding the block in create-block. ([57197](https://github.com/WordPress/gutenberg/pull/57197)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @akasunil @benazeer-ben @bph @Chrico @ciampo @d-alleyne @DAreRodz @dhruvikpatel18 @draganescu @ellatrix @fabiankaegy @fushar @getdave @gigitux @gziolo @hbhalodia @himanshupathak95 @im3dabasia @Infinite-Null @jameskoster @jasmussen @jeryj @jorgefilipecosta @jsnajdr @juanfra @justlevine @karthick-murugan @kmanijak @louwie17 @Lovor01 @Mamaduka @manzoorwanijk @matiasbenedetto @Mayank-Tripathi32 @mayurprajapatii @mcsf @michalczaplinski @mikachan @mirka @ntsekouras @oandregal @ockham @PARTHVATALIYA @prasadkarmalkar @ramonjd @rilwis @rinkalpagdar @Rishit30G @rohitmathur-7 @SainathPoojary @sarthaknagoshe2002 @SH4LIN @shail-mehta @shimotmk @sirreal @stokesman @Sukhendu2002 @swissspidy @t-hamano @talldan @tellthemachines @timse201 @tyxla @up1512001 @vampdroid @Vrishabhsk @yogeshbhutkar @youknowriad + + += 19.9.0 = + +## Changelog + +### Enhancements + +- Feature: Add `navigation.isLoading` state to core/router store. ([67680](https://github.com/WordPress/gutenberg/pull/67680)) +- Update the title, description, and order of Experiments page. ([67762](https://github.com/WordPress/gutenberg/pull/67762)) +- wp-env: Add phpMyAdmin support. ([67588](https://github.com/WordPress/gutenberg/pull/67588)) + +#### Components +- Added enableAlpha prop to CustomGradientPicker and GradientPicker components. ([66974](https://github.com/WordPress/gutenberg/pull/66974)) +- BorderBoxControl: Reduce gap value when unlinked. ([67049](https://github.com/WordPress/gutenberg/pull/67049)) +- DateTime: Add default date/time to stories. ([67678](https://github.com/WordPress/gutenberg/pull/67678)) +- Deprecate `COLORS.white`. ([67649](https://github.com/WordPress/gutenberg/pull/67649)) +- Disabled: Suppress `contentEditable` warning in story. ([67679](https://github.com/WordPress/gutenberg/pull/67679)) +- Document layout in Storybook. ([67628](https://github.com/WordPress/gutenberg/pull/67628)) +- DropdownMenu: Increase option height to 40px. ([67435](https://github.com/WordPress/gutenberg/pull/67435)) +- DuotonePicker: Simplify Button styles. ([66641](https://github.com/WordPress/gutenberg/pull/66641)) +- Menu: Throw when subcomponents are not rendered inside top level Menu. ([67411](https://github.com/WordPress/gutenberg/pull/67411)) +- Popover: Use `anchor` instead of `anchorRef` in story. ([67674](https://github.com/WordPress/gutenberg/pull/67674)) +- Storybook: Remove unnecessary feature flags. ([67576](https://github.com/WordPress/gutenberg/pull/67576)) +- Storybook: Update `ArgsTable` to `Controls` in preview. ([67582](https://github.com/WordPress/gutenberg/pull/67582)) +- Storybook: Update control types from `null` to `undefined`. ([67581](https://github.com/WordPress/gutenberg/pull/67581)) +- Storybook: Use manager-api instead of addons package. ([67578](https://github.com/WordPress/gutenberg/pull/67578)) +- Update @ariakit/react to 0.4.13. ([65907](https://github.com/WordPress/gutenberg/pull/65907)) +- Update @ariakit/react to 0.4.15 and @ariakit/test to 0.4.7. ([67404](https://github.com/WordPress/gutenberg/pull/67404)) + +#### Block Library +- Cover Block: Image size option for featured image. ([67273](https://github.com/WordPress/gutenberg/pull/67273)) +- Feature: Allow Post Template block to get deeply nested within Query Block. ([67657](https://github.com/WordPress/gutenberg/pull/67657)) +- Image Block: Change how the Image's overlay styles are applied. ([67788](https://github.com/WordPress/gutenberg/pull/67788)) +- Navigation: Enable all non-interactive formats. ([67585](https://github.com/WordPress/gutenberg/pull/67585)) +- Query block: Move patterns modal to dropdown on block toolbar. ([66993](https://github.com/WordPress/gutenberg/pull/66993)) +- Separator block: Allow divs to be used as separators. ([67530](https://github.com/WordPress/gutenberg/pull/67530)) +- New Block: Add Query Total block for displaying total query results or ranges. ([67629](https://github.com/WordPress/gutenberg/pull/67629)) +- Block Library: Update the relationship of `No results` block to `ancestor`. ([48348](https://github.com/WordPress/gutenberg/pull/48348)) + +#### DataViews +- Add header to the quick edit when bulk editing. ([67390](https://github.com/WordPress/gutenberg/pull/67390)) +- Data views: Expand configuration drop down on mobile. ([67715](https://github.com/WordPress/gutenberg/pull/67715)) +- Quick Edit: Add Template field. ([66591](https://github.com/WordPress/gutenberg/pull/66591)) +- Refactor actions to render modal outside of the menu. ([67664](https://github.com/WordPress/gutenberg/pull/67664)) +- Renders `DataForm` component only when data has been fetched. ([67694](https://github.com/WordPress/gutenberg/pull/67694)) +- Unify layout configuration. ([67477](https://github.com/WordPress/gutenberg/pull/67477)) +- Update bulk header with actions. ([67743](https://github.com/WordPress/gutenberg/pull/67743)) + +#### Style Book +- Add stylebook screen for classic themes. ([66851](https://github.com/WordPress/gutenberg/pull/66851)) +- Scroll to top at styles root. ([67605](https://github.com/WordPress/gutenberg/pull/67605)) +- Stylebook: Render overview colors in 4 columns. ([67597](https://github.com/WordPress/gutenberg/pull/67597)) +- Update style book headings to new design. ([67546](https://github.com/WordPress/gutenberg/pull/67546)) + +#### Post Editor +- Inline Commenting: Added new sidebar as extension of the canvas. ([67347](https://github.com/WordPress/gutenberg/pull/67347)) +- Inline Commenting: Re-order the comments in sidebar in which blocks are listed. ([66927](https://github.com/WordPress/gutenberg/pull/66927)) +- Inline commenting: UX Enhancements for Comments. ([67385](https://github.com/WordPress/gutenberg/pull/67385)) + +#### Site Editor +- Data Views: Add action for pages to set site homepage. ([65426](https://github.com/WordPress/gutenberg/pull/65426)) +- Sidebar: Update appearance of active items. ([67318](https://github.com/WordPress/gutenberg/pull/67318)) +- Style the selected template pattern. ([65917](https://github.com/WordPress/gutenberg/pull/65917)) + +#### Data Layer +- Data: Expose 'useSelect' warning to third-party consumers. ([67735](https://github.com/WordPress/gutenberg/pull/67735)) +- Data: Include more details when shallow equality fails in 'useSelect'. ([67713](https://github.com/WordPress/gutenberg/pull/67713)) + +#### Global Styles +- Controls in grid should match between sidebar panel and editor. ([67602](https://github.com/WordPress/gutenberg/pull/67602)) +- Shadows: Improve design and a11y of remove button. ([67705](https://github.com/WordPress/gutenberg/pull/67705)) + +#### Block Editor +- Prefer exact matches in Link Search results sorting. ([67367](https://github.com/WordPress/gutenberg/pull/67367)) +- Try direct drag (outside text editable). ([67305](https://github.com/WordPress/gutenberg/pull/67305)) + +#### Zoom Out +- Keep only copy, duplicate and delete in the zoom out more block toolbar menu item. ([67279](https://github.com/WordPress/gutenberg/pull/67279)) + +#### Font Library +- FontCollection: Update pagination controls. ([67143](https://github.com/WordPress/gutenberg/pull/67143)) + +#### Colors +- Add reset button to color control. ([67116](https://github.com/WordPress/gutenberg/pull/67116)) + + +### Bug Fixes + +- Exclude Set instance methods from polyfills. ([67230](https://github.com/WordPress/gutenberg/pull/67230)) +- Preload: Fix settings fields order. ([67450](https://github.com/WordPress/gutenberg/pull/67450)) +- Scripts: Make React Fast Refresh work with multiple blocks. ([64924](https://github.com/WordPress/gutenberg/pull/64924)) +- WP Scripts: Update webpack dependencies related to styling. ([67572](https://github.com/WordPress/gutenberg/pull/67572)) + +#### Site Editor +- Allow access to quick edit. ([67469](https://github.com/WordPress/gutenberg/pull/67469)) +- Edit Site: Fix sidebar template author navigation. ([67382](https://github.com/WordPress/gutenberg/pull/67382)) +- Fix Site editor navigation menu items alignment visual regression. ([67321](https://github.com/WordPress/gutenberg/pull/67321)) +- Fix sidebar item animation regression. ([67771](https://github.com/WordPress/gutenberg/pull/67771)) +- Fix sidebar plugins. ([67557](https://github.com/WordPress/gutenberg/pull/67557)) +- Fix the templates route on mobile. ([67547](https://github.com/WordPress/gutenberg/pull/67547)) +- Fix: Fixed site-editor crashing when added front-page template and clicking more option. ([67500](https://github.com/WordPress/gutenberg/pull/67500)) +- Fix: Fixed styling tab not opening on themes without style variations on mobile & desktop. ([67537](https://github.com/WordPress/gutenberg/pull/67537)) +- Preload: Parse post ID from p (path). ([67465](https://github.com/WordPress/gutenberg/pull/67465)) +- Remove default page slug. ([67673](https://github.com/WordPress/gutenberg/pull/67673)) +- Router: Fix addition and removal of empty classnames. ([67378](https://github.com/WordPress/gutenberg/pull/67378)) +- Wrap each router area in 'ErrorBoundary'. ([64245](https://github.com/WordPress/gutenberg/pull/64245)) +- useEditorTitle: Fix wrong request without ID. ([67475](https://github.com/WordPress/gutenberg/pull/67475)) + +#### Block Editor +- Animate `useScaleCanvas()` only when toggling zoomed out mode. ([67481](https://github.com/WordPress/gutenberg/pull/67481)) +- Drag and drop: Fix drop zones on block drag. ([67317](https://github.com/WordPress/gutenberg/pull/67317)) +- Drag and drop: Fix firefox compat logic. ([67439](https://github.com/WordPress/gutenberg/pull/67439)) +- Fix JS error in the 'useTabNav' hook. ([67102](https://github.com/WordPress/gutenberg/pull/67102)) +- FontFamilyControl: Restore margin bottom. ([67424](https://github.com/WordPress/gutenberg/pull/67424)) +- Inserter: Hide child blocks from the inserter when needed. ([67734](https://github.com/WordPress/gutenberg/pull/67734)) +- Inserter: Patterns: Remove loading indicator. ([67072](https://github.com/WordPress/gutenberg/pull/67072)) +- Inserter: Should receive focus on open. ([67754](https://github.com/WordPress/gutenberg/pull/67754)) +- Remove words count in the multi-selection inspector. ([67624](https://github.com/WordPress/gutenberg/pull/67624)) +- Storybook: Fix `BlockPatternsList` fixtures. ([67672](https://github.com/WordPress/gutenberg/pull/67672)) +- Drag and drop: Fix misplaced drop indicator. ([67434](https://github.com/WordPress/gutenberg/pull/67434)) +- Drag and drop: Fix scroll disorientation after drop. ([67405](https://github.com/WordPress/gutenberg/pull/67405)) +- Drag and drop: Restore moving animation. ([67417](https://github.com/WordPress/gutenberg/pull/67417)) + +#### Block Library +- Align Submenu block and Nav Link block by including description and wrapping span. ([67198](https://github.com/WordPress/gutenberg/pull/67198)) +- CommentsPagination: Set font-size to inherit for pagination items. ([67296](https://github.com/WordPress/gutenberg/pull/67296)) +- Fix latest post block spacing issue. ([66442](https://github.com/WordPress/gutenberg/pull/66442)) +- Fix: Caption with Link in Wide-Width and Full-Width Images Appears on two lines. ([67392](https://github.com/WordPress/gutenberg/pull/67392)) +- Fix: Don't show `aria-label` when its value is empty. ([67381](https://github.com/WordPress/gutenberg/pull/67381)) +- Navigation Block: Fix issue with double-clicking "Create a new menu" causing duplicate menus. ([67488](https://github.com/WordPress/gutenberg/pull/67488)) +- Pullquote block having design issue when text-decoration is choosen strikethrough. ([66707](https://github.com/WordPress/gutenberg/pull/66707)) +- Remove inline-block display from image anchor in style.scss. ([67368](https://github.com/WordPress/gutenberg/pull/67368)) +- Search block: Add space between attributes when using "Button only" option. ([61399](https://github.com/WordPress/gutenberg/pull/61399)) +- Updated 'Set featured image' text in dropdown. ([67775](https://github.com/WordPress/gutenberg/pull/67775)) + +#### DataViews +- Avoid double click handler on primary fields. ([67393](https://github.com/WordPress/gutenberg/pull/67393)) +- Better handling of missing onClickItem prop. ([67402](https://github.com/WordPress/gutenberg/pull/67402)) +- Fix filters lost when switching layouts. ([67740](https://github.com/WordPress/gutenberg/pull/67740)) +- Fix hidden List layout actions dropdown. ([67778](https://github.com/WordPress/gutenberg/pull/67778)) +- Fix reordering fields in list and grid layouts. ([67777](https://github.com/WordPress/gutenberg/pull/67777)) +- Fix: Duplicate template part refers to original name instead of duplicated name. ([67329](https://github.com/WordPress/gutenberg/pull/67329)) +- Preserve filters when switching layouts in templates dataviews. ([67744](https://github.com/WordPress/gutenberg/pull/67744)) +- QuickEdit: Prevent site-editor from crashing when slug is not an object. ([67577](https://github.com/WordPress/gutenberg/pull/67577)) +- Site Editor: Fix featured image not appearing in pages dataviews. ([67562](https://github.com/WordPress/gutenberg/pull/67562)) + +#### Components +- CustomSelectControl: Update Value from Fresh State. ([67733](https://github.com/WordPress/gutenberg/pull/67733)) +- Fix the 'ClipboardButton' effect cleanup. ([67399](https://github.com/WordPress/gutenberg/pull/67399)) +- Navigation: Fix active item hover color. ([67732](https://github.com/WordPress/gutenberg/pull/67732)) +- Scrollable: Fix story by declaring field as readonly. ([67683](https://github.com/WordPress/gutenberg/pull/67683)) +- Storybook: Fix control types. ([67646](https://github.com/WordPress/gutenberg/pull/67646)) +- Storybook: Fix storybook blocks imports. ([67684](https://github.com/WordPress/gutenberg/pull/67684)) +- Storybook: Fix table markup in Design Language - Radius documentation. ([67686](https://github.com/WordPress/gutenberg/pull/67686)) +- Theme: Fix contrast in nested story. ([67681](https://github.com/WordPress/gutenberg/pull/67681)) + +#### Post Editor +- Fix Meta boxes saving when they’re not present. ([67254](https://github.com/WordPress/gutenberg/pull/67254)) +- Fix hiding and showing of meta boxes. ([67504](https://github.com/WordPress/gutenberg/pull/67504)) +- Fix: Header layout spacing in Firefox. ([67074](https://github.com/WordPress/gutenberg/pull/67074)) +- Make sure Document Bar doesn’t go missing. ([67322](https://github.com/WordPress/gutenberg/pull/67322)) +- Update pre-publish panel wording to accurately describe the review process. ([67328](https://github.com/WordPress/gutenberg/pull/67328)) + +#### Zoom Out +- Fix for inserter. ([67495](https://github.com/WordPress/gutenberg/pull/67495)) +- Fix useZoomOut inserter behavior. ([67591](https://github.com/WordPress/gutenberg/pull/67591)) +- Fix zoom animation scrollbar. ([67536](https://github.com/WordPress/gutenberg/pull/67536)) +- UseScaleCanvas performance improvements. ([67496](https://github.com/WordPress/gutenberg/pull/67496)) + +#### Write mode +- Fix color of disabled buttons in dark toolbar. ([67348](https://github.com/WordPress/gutenberg/pull/67348)) +- Fix synced pattern editing in write mode and refactor block editing mode to reducer. ([67026](https://github.com/WordPress/gutenberg/pull/67026)) +- Fix: Remove parent block selector while in Write mode. ([67395](https://github.com/WordPress/gutenberg/pull/67395)) +- Fix: Write Mode mode persists as enabled in widget editor. ([67587](https://github.com/WordPress/gutenberg/pull/67587)) + +#### Global Styles +- Edit site: Remove empty preview border and redirect to editor in global styles navigation. ([67548](https://github.com/WordPress/gutenberg/pull/67548)) +- Fix: Styles section does not moves stylebook to typography. ([67423](https://github.com/WordPress/gutenberg/pull/67423)) +- Global Styles Preview: Don't use iframe component. ([67682](https://github.com/WordPress/gutenberg/pull/67682)) + +#### Style Book +- Fix critical error when blocks are not registered. ([67703](https://github.com/WordPress/gutenberg/pull/67703)) + +#### Design Tools +- Global Styles: Fix handling of booleans when stabilizing block supports. ([67552](https://github.com/WordPress/gutenberg/pull/67552)) + +#### Block bindings +- Revert "Extensibility: Make Block Bindings work with `editor.BlockEdit` hook". ([67516](https://github.com/WordPress/gutenberg/pull/67516)) + +#### Patterns +- Site Editor: Fix the patterns route on mobile. ([67467](https://github.com/WordPress/gutenberg/pull/67467)) + +#### Focus Mode +- Site Editor: Fix focus mode navigation. ([67458](https://github.com/WordPress/gutenberg/pull/67458)) + +#### List View +- Fix List View not updating when switching editor modes. ([67379](https://github.com/WordPress/gutenberg/pull/67379)) + +#### Extensibility +- Make Block Bindings work with `editor.BlockEdit` hook. ([67370](https://github.com/WordPress/gutenberg/pull/67370)) + +#### Synced Patterns +- Remove use of `contentOnly` block editing mode for synced patterns. ([67364](https://github.com/WordPress/gutenberg/pull/67364)) + +#### Widgets Editor +- Block Bindings: Remove client core sources registration in widgets. ([67349](https://github.com/WordPress/gutenberg/pull/67349)) + +#### REST API +- Support search_columns argument in the user endpoint. ([67330](https://github.com/WordPress/gutenberg/pull/67330)) + + +### Accessibility + +- [Dataviews] Fix: Space does not triggers the media button on grid view. ([67791](https://github.com/WordPress/gutenberg/pull/67791)) + +#### Block Editor +- BlockSwitcher: Refactor to use Button layout properly. ([67502](https://github.com/WordPress/gutenberg/pull/67502)) +- Remove one occurrence of incorrect usage of ItemGroup. ([67427](https://github.com/WordPress/gutenberg/pull/67427)) + +#### DataViews +- [a11y] Fix: Media button on the page view grid does not have an accessible name. ([67690](https://github.com/WordPress/gutenberg/pull/67690)) + +#### Components +- Fix incorrect usage of ItemGroup in the Image block filters panel. ([67513](https://github.com/WordPress/gutenberg/pull/67513)) + +#### Post Editor +- Fix EntitiesSavedStates panel dialog props. ([67351](https://github.com/WordPress/gutenberg/pull/67351)) + + +### Performance + +- Fix re-renders caused by `getEntityRecordsPermissions` after #67667. ([67770](https://github.com/WordPress/gutenberg/pull/67770)) +- Preload: Fix end-to-end test. ([67497](https://github.com/WordPress/gutenberg/pull/67497)) +- Site Editor: Pages: Preload template lookup. ([66654](https://github.com/WordPress/gutenberg/pull/66654)) +- [mini] Preload: Add post type. ([67518](https://github.com/WordPress/gutenberg/pull/67518)) + + +### Experiments + +- Move `duplicateTemplatePart` action to the `@wordpress/fields` package. ([65390](https://github.com/WordPress/gutenberg/pull/65390)) + + +### Documentation + +- Button: Revise documentation. ([66617](https://github.com/WordPress/gutenberg/pull/66617)) +- Docs: Fix Playwright Page Object Model link. ([67652](https://github.com/WordPress/gutenberg/pull/67652)) +- Docs: Include the strategy for setting `engines` for WordPress packages. ([67727](https://github.com/WordPress/gutenberg/pull/67727)) +- Docs: Remove invalid key projects links on the documentation. ([67491](https://github.com/WordPress/gutenberg/pull/67491)) +- Improve documentation for fields package. ([67580](https://github.com/WordPress/gutenberg/pull/67580)) +- Refine `getServerState()` & `getServerContext()` documentation. ([67499](https://github.com/WordPress/gutenberg/pull/67499)) +- Storybook: Add WritingModeControl story. ([67343](https://github.com/WordPress/gutenberg/pull/67343)) +- Storybook: Add stories for AlignmentToolbar and AlignmentControl components. ([67046](https://github.com/WordPress/gutenberg/pull/67046)) +- Storybook: Add stories for HeadingLevelDropdown component. ([67294](https://github.com/WordPress/gutenberg/pull/67294)) +- Storybook: Revert "Preview: ArgsTable => Controls (#67582)". ([67656](https://github.com/WordPress/gutenberg/pull/67656)) +- Storybook: Support keyword search in Icon Library. ([67442](https://github.com/WordPress/gutenberg/pull/67442)) +- Switch Several Links to https in Document Files. ([67706](https://github.com/WordPress/gutenberg/pull/67706)) +- Update README.md. ([67711](https://github.com/WordPress/gutenberg/pull/67711)) +- Update extending-the-query-loop-block.md. ([67529](https://github.com/WordPress/gutenberg/pull/67529)) +- Update global stylesheet docblocks with `custom-css` parameter. ([67716](https://github.com/WordPress/gutenberg/pull/67716)) +- Updated old URL in Documentation. ([67446](https://github.com/WordPress/gutenberg/pull/67446)) + + +### Code Quality + +- Convert lock unlock to generics. ([66682](https://github.com/WordPress/gutenberg/pull/66682)) +- CreateTemplatePartModal: Avoid identity warning in useSelect. ([67786](https://github.com/WordPress/gutenberg/pull/67786)) +- CreateTemplatePartModal: Replace `ts-ignore` with `ts-expect-error`. ([67709](https://github.com/WordPress/gutenberg/pull/67709)) +- Fix misc type compilation errors in editor and block editor packages. ([67410](https://github.com/WordPress/gutenberg/pull/67410)) +- Fix: Invalid JSDoc for optional string parameter and return value. ([67489](https://github.com/WordPress/gutenberg/pull/67489)) +- Fix: Remove unused test code on tools panel. ([67589](https://github.com/WordPress/gutenberg/pull/67589)) +- Removed trailing space in "Color randomizer ". ([67457](https://github.com/WordPress/gutenberg/pull/67457)) +- Update misc types and revert WPCompleter export from components. ([67599](https://github.com/WordPress/gutenberg/pull/67599)) + +#### Components +- BoxControl: Deprecate 36px default size. ([66704](https://github.com/WordPress/gutenberg/pull/66704)) +- BoxControl: Passive deprecate `onMouseOver`/`onMouseOut`. ([67332](https://github.com/WordPress/gutenberg/pull/67332)) +- BoxControl: Refactor and unify the different sides implementation. ([67626](https://github.com/WordPress/gutenberg/pull/67626)) +- CustomSelectControl: Deprecate 36px default size. ([67441](https://github.com/WordPress/gutenberg/pull/67441)) +- FormFileUpload: Deprecate 36px default size. ([67438](https://github.com/WordPress/gutenberg/pull/67438)) +- FormTokenField: Deprecate 36px default size. ([67454](https://github.com/WordPress/gutenberg/pull/67454)) +- NumberControl: Deprecate 36px default size. ([66730](https://github.com/WordPress/gutenberg/pull/66730)) +- RangeControl: Update the default marks styles to match the padding/margin control. ([67611](https://github.com/WordPress/gutenberg/pull/67611)) +- Remove `__unstableMotionContext` from `@wordpress/components`. ([67623](https://github.com/WordPress/gutenberg/pull/67623)) +- SlotFill: Remove explicit rerender from portal version. ([67471](https://github.com/WordPress/gutenberg/pull/67471)) +- Tabs: Overhaul unit tests. ([66140](https://github.com/WordPress/gutenberg/pull/66140)) +- ToolbarButton: Set size to "compact". ([67440](https://github.com/WordPress/gutenberg/pull/67440)) +- UnitControl : Deprecate 36px default size. ([66791](https://github.com/WordPress/gutenberg/pull/66791)) + +#### Block Editor +- Group 'onRemove' callback with other public APIs. ([67551](https://github.com/WordPress/gutenberg/pull/67551)) +- InspectorControlsSlot: Remove unused framer motion context forwarding. ([67522](https://github.com/WordPress/gutenberg/pull/67522)) +- LetteringSpacingControl: Deprecate 36px default size. ([67429](https://github.com/WordPress/gutenberg/pull/67429)) +- Reduce the 'isZoomOut' selector calls in the block toolbar. ([67594](https://github.com/WordPress/gutenberg/pull/67594)) +- Remove 'React.Children' legacy API in 'Warning' component. ([67675](https://github.com/WordPress/gutenberg/pull/67675)) +- Replace remaining custom deep cloning with 'structuredClone'. ([67707](https://github.com/WordPress/gutenberg/pull/67707)) +- Stabilize `LinkControl` Component. ([56384](https://github.com/WordPress/gutenberg/pull/56384)) + +#### Site Editor +- Remove .components-item-group selector in edit-site components[2]. ([67575](https://github.com/WordPress/gutenberg/pull/67575)) +- Site Editor Sidebar: Remove `hasGlobalStyleVariations` condition for the Styles nav item. ([67545](https://github.com/WordPress/gutenberg/pull/67545)) +- Unify layout with posts dataviews. ([67162](https://github.com/WordPress/gutenberg/pull/67162)) +- Use path based routing instead of query args and site-editor.php routes. ([67199](https://github.com/WordPress/gutenberg/pull/67199)) + +#### Post Editor +- Editor: Refactor 'PostPublishPanelPostpublish' to function component. ([67398](https://github.com/WordPress/gutenberg/pull/67398)) +- Editor: Use hooks instead of HOC in 'PostPublishButtonOrToggle'. ([67413](https://github.com/WordPress/gutenberg/pull/67413)) +- Remove PostSlugCheck and PostSlug unused components. ([67414](https://github.com/WordPress/gutenberg/pull/67414)) + +#### DataViews +- Create a single component for rendering the actions list. ([67558](https://github.com/WordPress/gutenberg/pull/67558)) +- Fix: Dataviews remove primary field concept from some classes. ([67689](https://github.com/WordPress/gutenberg/pull/67689)) + +#### Data Layer +- TypeScript: Convert factory utils in data package to TS. ([67667](https://github.com/WordPress/gutenberg/pull/67667)) + +#### Shortcodes +- Add types for shortcode package. ([67416](https://github.com/WordPress/gutenberg/pull/67416)) + +#### Block bindings +- Remove fallback for `context.postType` in post meta. ([67345](https://github.com/WordPress/gutenberg/pull/67345)) + +#### Block hooks +- Navigation block: Remove more obsolete Block Hooks helpers. ([67193](https://github.com/WordPress/gutenberg/pull/67193)) + + +### Tools + +- PR template: Add before/after table. ([62739](https://github.com/WordPress/gutenberg/pull/62739)) + +#### Build Tooling +- Build: Stop generating unused legacy scripts for core blocks. ([65268](https://github.com/WordPress/gutenberg/pull/65268)) +- CI: Skip native jobs. ([67799](https://github.com/WordPress/gutenberg/pull/67799)) +- DataViews build-wp: Don't bundle singleton WordPress packages. ([67590](https://github.com/WordPress/gutenberg/pull/67590)) +- DataViews build-wp: Don't bundle the date package. ([67612](https://github.com/WordPress/gutenberg/pull/67612)) +- Keycodes: Improve tree shaking by annotating exports as pure. ([67615](https://github.com/WordPress/gutenberg/pull/67615)) +- Upgrade TypeScript to 5.7 and fix types. ([67461](https://github.com/WordPress/gutenberg/pull/67461)) +- Combine the release steps to ensure that releases are tagged. ([65591](https://github.com/WordPress/gutenberg/pull/65591)) + +#### Testing +- e2e-test-utils-playwright: Increase timeout of site-editor selector. ([66672](https://github.com/WordPress/gutenberg/pull/66672)) + + +### Security + +#### npm Packages +- Update npm dependencies to fix issues reported by audit. ([67708](https://github.com/WordPress/gutenberg/pull/67708)) + + +### Various + +#### Extensibility +- Add ability to show drop cap setting in paragraph block by default. ([45994](https://github.com/WordPress/gutenberg/pull/45994)) +- DataViews: Move template and pattern title fields. ([67449](https://github.com/WordPress/gutenberg/pull/67449)) +- DataViews: Update `usePostFields` to accept postType. ([67380](https://github.com/WordPress/gutenberg/pull/67380)) + +#### Plugin +- Only override REST server for older WP versions. ([67779](https://github.com/WordPress/gutenberg/pull/67779)) + +#### NUX +- Welcome guide headline update. ([67654](https://github.com/WordPress/gutenberg/pull/67654)) + +#### Block Locking +- Simplify description and option names in the Lock modal dialog. ([67437](https://github.com/WordPress/gutenberg/pull/67437)) + + +## First-time contributors + +The following PRs were merged by first-time contributors: + +- @alexflorisca: e2e-test-utils-playwright: Increase timeout of site-editor selector. ([66672](https://github.com/WordPress/gutenberg/pull/66672)) +- @benazeer-ben: Site editor: Style the selected template pattern. ([65917](https://github.com/WordPress/gutenberg/pull/65917)) +- @creador-dev: Navigation Block: Fix issue with double-clicking "Create a new menu" causing duplicate menus. ([67488](https://github.com/WordPress/gutenberg/pull/67488)) +- @dknauss: Update README.md. ([67711](https://github.com/WordPress/gutenberg/pull/67711)) +- @im3dabasia: Removed trailing space in "Color randomizer ". ([67457](https://github.com/WordPress/gutenberg/pull/67457)) +- @Mayank-Tripathi32: Fix: Header layout spacing in Firefox. ([67074](https://github.com/WordPress/gutenberg/pull/67074)) +- @subodhr258: CustomSelectControl: Update Value from Fresh State. ([67733](https://github.com/WordPress/gutenberg/pull/67733)) +- @wwdes: Added enableAlpha prop to CustomGradientPicker and GradientPicker components. ([66974](https://github.com/WordPress/gutenberg/pull/66974)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @akasunil @alexflorisca @annezazu @benazeer-ben @ciampo @creador-dev @creativecoder @DAreRodz @dcalhoun @dd32 @dknauss @draganescu @ellatrix @fabiankaegy @getdave @gigitux @gvgvgvijayan @gziolo @hbhalodia @im3dabasia @imrraaj @jameskoster @jeryj @jorgefilipecosta @jsnajdr @juanfra @louwie17 @Mamaduka @manzoorwanijk @matiasbenedetto @Mayank-Tripathi32 @mcsf @michalczaplinski @miminari @mirka @ntsekouras @oandregal @ockham @prajapatisagar @ramonjd @sabernhardt @SantosGuillamot @sarthaknagoshe2002 @sgomes @shail-mehta @stokesman @subodhr258 @Sukhendu2002 @t-hamano @talldan @tellthemachines @tyxla @viralsampat-multidots @wwdes @yogeshbhutkar @youknowriad + + + + = 19.8.0 = ## Changelog diff --git a/docs/README.md b/docs/README.md index 31471a9928b2cf..4fd7d16595e133 100644 --- a/docs/README.md +++ b/docs/README.md @@ -48,7 +48,7 @@ This handbook should be considered the canonical resource for all things related ## Are you in the right place? -The Block Editor Handbook is designed for those looking to create and develop for the Block Editor. However, it's important to note that there are multiple other handbooks available within the [Developer Resources](http://developer.wordpress.org/) that you may find beneficial: +The Block Editor Handbook is designed for those looking to create and develop for the Block Editor. However, it's important to note that there are multiple other handbooks available within the [Developer Resources](https://developer.wordpress.org/) that you may find beneficial: - [Theme Handbook](https://developer.wordpress.org/themes) - [Plugin Handbook](https://developer.wordpress.org/plugins) diff --git a/docs/explanations/architecture/styles.md b/docs/explanations/architecture/styles.md index 5f5e73d1372f7b..68f09f04d21d32 100644 --- a/docs/explanations/architecture/styles.md +++ b/docs/explanations/architecture/styles.md @@ -37,10 +37,8 @@ The user may change the state of this block by applying different styles: a text After some user modifications to the block, the initial markup may become something like this: ```html -

+

``` This is what we refer to as "user-provided block styles", also know as "local styles" or "serialized styles". Essentially, each tool (font size, color, etc) ends up adding some classes and/or inline styles to the block markup. The CSS styling for these classes is part of the block, global, or theme stylesheets. @@ -125,7 +123,7 @@ The block supports API only serializes the font size value to the wrapper, resul This is an active area of work you can follow [in the tracking issue](https://github.com/WordPress/gutenberg/issues/38167). The linked proposal is exploring a different way to serialize the user changes: instead of each block support serializing its own data (for example, classes such as `has-small-font-size`, `has-green-color`) the idea is the block would get a single class instead (for example, `wp-style-UUID`) and the CSS styling for that class will be generated in the server by WordPress. -While work continues in that proposal, there's an escape hatch, an experimental option block authors can use. Any block support can skip the serialization to HTML markup by using `skipSerialization`. For example: +While work continues in that proposal, there's an escape hatch, an experimental option block authors can use. Any block support can skip the serialization to HTML markup by using `__experimentalSkipSerialization`. For example: ```json { @@ -134,7 +132,7 @@ While work continues in that proposal, there's an escape hatch, an experimental "supports": { "typography": { "fontSize": true, - "skipSerialization": true + "__experimentalSkipSerialization": true } } } @@ -142,7 +140,7 @@ While work continues in that proposal, there's an escape hatch, an experimental This means that the typography block support will do all of the things (create a UI control, bind the block attribute to the control, etc) except serializing the user values into the HTML markup. The classes and inline styles will not be automatically applied to the wrapper and it is the block author's responsibility to implement this in the `edit`, `save`, and `render_callback` functions. See [this issue](https://github.com/WordPress/gutenberg/issues/28913) for examples of how it was done for some blocks provided by WordPress. -Note that, if `skipSerialization` is enabled for a group (typography, color, spacing) it affects _all_ block supports within this group. In the example above _all_ the properties within the `typography` group will be affected (e.g. `fontSize`, `lineHeight`, `fontFamily` .etc). +Note that, if `__experimentalSkipSerialization` is enabled for a group (typography, color, spacing) it affects _all_ block supports within this group. In the example above _all_ the properties within the `typography` group will be affected (e.g. `fontSize`, `lineHeight`, `fontFamily` .etc). To enable for a _single_ property only, you may use an array to declare which properties are to be skipped. In the example below, only `fontSize` will skip serialization, leaving other items within the `typography` group (e.g. `lineHeight`, `fontFamily` .etc) unaffected. @@ -154,7 +152,7 @@ To enable for a _single_ property only, you may use an array to declare which pr "typography": { "fontSize": true, "lineHeight": true, - "skipSerialization": [ "fontSize" ] + "__experimentalSkipSerialization": [ "fontSize" ] } } } @@ -475,7 +473,7 @@ If blocks do this, they need to be registered in the server using the `block.jso Every chunk of styles can only use a single selector. -This is particularly relevant if the block is using `skipSerialization` to serialize the different style properties to different nodes other than the wrapper. See "Current limitations of blocks supports" for more. +This is particularly relevant if the block is using `__experimentalSkipSerialization` to serialize the different style properties to different nodes other than the wrapper. See "Current limitations of blocks supports" for more. #### 3. **Only a single property per block** diff --git a/docs/getting-started/devenv/get-started-with-wp-env.md b/docs/getting-started/devenv/get-started-with-wp-env.md index 74942ea3ee93bf..a6427deb863b7e 100644 --- a/docs/getting-started/devenv/get-started-with-wp-env.md +++ b/docs/getting-started/devenv/get-started-with-wp-env.md @@ -47,7 +47,7 @@ wp-env start Once the script completes, you can access the local environment at: http://localhost:8888. Log into the WordPress dashboard using username `admin` and password `password`.
- Some projects, like Gutenberg, include their own specific wp-env configurations, and the documentation might prompt you to run npm run start wp-env instead. + Some projects, like Gutenberg, include their own specific wp-env configurations, and the documentation might prompt you to run npm run wp-env start instead.
For more information on controlling the Docker environment, see the [@wordpress/env package](/packages/env/README.md) readme. diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index 8ac489e3c154a2..d9120cc58197e9 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -8,7 +8,7 @@ What follows is a set of questions that have come up from the last few years of “Gutenberg” is the name of the project to create a new editor experience for WordPress — contributors have been working on it since January 2017 and it’s one of the most significant changes to WordPress in years. It’s built on the idea of using “blocks” to write and design posts and pages. This will serve as the foundation for future improvements to WordPress, including blocks as a way not just to design posts and pages, but also entire sites. The overall goal is to simplify the first-time user experience of WordPress — for those who are writing, editing, publishing, and designing web pages. The editing experience is intended to give users a better visual representation of what their post or page will look like when they hit publish. Originally, this was the kickoff goal: -> The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery. +> The editor will endeavor to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery. Key takeaways include the following points: diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md index 348b95ba88da3c..4cd7c0b36fe86a 100644 --- a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -26,7 +26,7 @@ The diagram below provides an overview of the build process when using the `wp-s - **Production Mode (`npm run build`):** In this mode, `wp-scripts` compiles your JavaScript, minifying the output to reduce file size and improve loading times in the browser. This is ideal for deploying your code to a live site. -- **Development Mode (`npm run start`):** This mode is tailored for active development. It skips minification for easier debugging, generates source maps for better error tracking, and watches your source files for changes. When a change is detected, it automatically rebuilds the affected files, allowing you to see updates in real-time. +- **Development Mode (`npm start`):** This mode is tailored for active development. It skips minification for easier debugging, generates source maps for better error tracking, and watches your source files for changes. When a change is detected, it automatically rebuilds the affected files, allowing you to see updates in real-time. The `wp-scripts` package also facilitates the use of JavaScript modules, allowing code distribution across multiple files and resulting in a streamlined bundle after the build process. The [block-development-example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) GitHub repository provides some good examples. @@ -38,9 +38,9 @@ The `wp-scripts` package also facilitates the use of JavaScript modules, allowin Integrating JavaScript into your WordPress projects without a build process can be the most straightforward approach in specific scenarios. This is particularly true for projects that don't leverage JSX or other advanced JavaScript features requiring compilation. -When you opt out of a build process, you interact directly with WordPress's [JavaScript APIs](/docs/reference-guides/packages/) through the global `wp` object. This means that all the methods and packages provided by WordPress are readily available, but with one caveat: you must manually manage script dependencies. This is done by adding [the handle](/docs/contributors/code/scripts.md) of each corresponding package to the dependency array of your enqueued JavaScript file. +When you opt out of a build process, you interact directly with WordPress's [JavaScript APIs](/docs/reference-guides/packages.md) through the global `wp` object. This means that all the methods and packages provided by WordPress are readily available, but with one caveat: you must manually manage script dependencies. This is done by adding [the handle](/docs/contributors/code/scripts.md) of each corresponding package to the dependency array of your enqueued JavaScript file. -For example, suppose you're creating a script that registers a new block [variation](/docs/reference-guides/block-api/block-variations.md) using the `registerBlockVariation` function from the [`blocks`](/docs/reference-guides/packages/packages-blocks.md) package. You must include `wp-blocks` in your script's dependency array. This guarantees that the `wp.blocks.registerBlockVariation` method is available and defined by the time your script executes. +For example, suppose you're creating a script that registers a new block [variation](/docs/reference-guides/block-api/block-variations.md) using the `registerBlockVariation` function from the [`blocks`](/packages/blocks/README.md) package. You must include `wp-blocks` in your script's dependency array. This guarantees that the `wp.blocks.registerBlockVariation` method is available and defined by the time your script executes. In the following example, the `wp-blocks` dependency is defined when enqueuing the `variations.js` file. diff --git a/docs/getting-started/fundamentals/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md index 5c80422f6f8574..63a7a9031f72a7 100644 --- a/docs/getting-started/fundamentals/registration-of-a-block.md +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -42,7 +42,7 @@ function minimal_block_ca6eda___register_block() { add_action( 'init', 'minimal_block_ca6eda___register_block' ); ``` -_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/index.php)_ +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/plugin.php)_ ## Registering a block with JavaScript (client-side) diff --git a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md index b90b4668530797..3c75e1e82668f2 100644 --- a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md +++ b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md @@ -70,7 +70,7 @@ By default this behavior is disabled until the `directInsert` prop is set to `tr ## Template -Use the template property to define a set of blocks that prefill the InnerBlocks component when inserted. You can set attributes on the blocks to define their use. The example below shows a book review template using InnerBlocks component and setting placeholders values to show the block usage. +Use the template property to define a set of blocks that prefill the InnerBlocks component when it has no existing content.. You can set attributes on the blocks to define their use. The example below shows a book review template using InnerBlocks component and setting placeholders values to show the block usage. ```js diff --git a/docs/how-to-guides/themes/global-settings-and-styles.md b/docs/how-to-guides/themes/global-settings-and-styles.md index f71bd67bfaf2ec..205a3ee862ce6b 100644 --- a/docs/how-to-guides/themes/global-settings-and-styles.md +++ b/docs/how-to-guides/themes/global-settings-and-styles.md @@ -1053,16 +1053,16 @@ Pseudo selectors `:hover`, `:focus`, `:visited`, `:active`, `:link`, `:any-link` #### Variations -A block can have a "style variation", as defined per the [block.json specification](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#styles-optional). Theme authors can define the style attributes for an existing style variation using the theme.json file. Styles for unregistered style variations will be ignored. +A block can have a "style variation," as defined in the [block.json specification](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/#styles-optional). Theme authors can define the style attributes for an existing style variation using the `theme.json` file. Styles for unregistered style variations will be ignored. -Note that variations are a "block concept", they only exist bound to blocks. The `theme.json` specification respects that distinction by only allowing `variations` at the block-level but not at the top-level. It's also worth highlighting that only variations defined in the `block.json` file of the block are considered "registered": so far, the style variations added via `register_block_style` or in the client are ignored, see [this issue](https://github.com/WordPress/gutenberg/issues/49602) for more information. +Note that variations are a "block concept"—they only exist when bound to blocks. The `theme.json` specification respects this distinction by only allowing `variations` at the block level, not the top level. It’s also worth highlighting that only variations defined in the `block.json` file of the block or via `register_block_style` on the server are considered "registered" for `theme.json` styling purposes. For example, this is how to provide styles for the existing `plain` variation for the `core/quote` block: ```json { "version": 3, - "styles":{ + "styles": { "blocks": { "core/quote": { "variations": { @@ -1078,7 +1078,7 @@ For example, this is how to provide styles for the existing `plain` variation fo } ``` -The resulting CSS output is this: +The resulting CSS output is: ```css .wp-block-quote.is-style-plain { @@ -1086,6 +1086,99 @@ The resulting CSS output is this: } ``` +It is also possible for multiple block types to share the same variation styles. There are two recommended ways to define such shared styles: + +1. `theme.json` partial files +2. programmatically, using `register_block_style` + +##### Variation Theme.json Partials + +Like theme style variation partials, those for block style variations reside within a theme's `/styles` directory. However, they are differentiated from theme style variations by the introduction of a top-level property called `blockTypes`. The `blockTypes` property is an array of block types for which the block style variation has been registered. + +Additionally, a `slug` property is available to provide consistency between the different sources that may define block style variations and to decouple the `slug` from the translatable `title` property. + +The following is an example of a `theme.json` partial that defines styles for the "Variation A" block style for the Group, Columns, and Media & Text block types: + +```json +{ + "$schema": "https://schemas.wp.org/trunk/theme.json", + "version": 3, + "title": "Variation A", + "slug": "variation-a", + "blockTypes": [ "core/group", "core/columns", "core/media-text" ], + "styles": { + "color": { + "background": "#eed8d3", + "text": "#201819" + }, + "elements": { + "heading": { + "color": { + "text": "#201819" + } + } + }, + "blocks": { + "core/group": { + "color": { + "background": "#825f58", + "text": "#eed8d3" + }, + "elements": { + "heading": { + "color": { + "text": "#eed8d3" + } + } + } + } + } + } +} +``` + +##### Programmatically Registering Variation Styles + +As an alternative to `theme.json` partials, you can register variation styles at the same time as registering the variation itself through `register_block_style`. This is done by registering the block style for an array of block types while also passing a "style object" within the `style_data` option. + +The example below registers a "Green" variation for the Group and Columns blocks. Note that the style object passed via `style_data` follows the same shape as the `styles` property of a `theme.json` partial. + +```php +register_block_style( + array( 'core/group', 'core/columns' ), + array( + 'name' => 'green', + 'label' => __( 'Green' ), + 'style_data' => array( + 'color' => array( + 'background' => '#4f6f52', + 'text' => '#d2e3c8', + ), + 'blocks' => array( + 'core/group' => array( + 'color' => array( + 'background' => '#739072', + 'text' => '#e3eedd', + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => '#ead196', + ), + ':hover' => array( + 'color' => array( + 'text' => '#ebd9b4', + ), + ), + ), + ), + ), + ) +); +``` + ### customTemplates
Supported in WordPress from version 5.9.
diff --git a/docs/private-apis.md b/docs/private-apis.md new file mode 100644 index 00000000000000..14c1a4aa22472b --- /dev/null +++ b/docs/private-apis.md @@ -0,0 +1,340 @@ +# Gutenberg Private APIs + +This is an overview of private APIs exposed by Gutenberg packages. These APIs are used to implement parts of the Gutenberg editor (Post Editor, Site Editor, Core blocks and others) but are not exposed publicly to plugin and theme authors or authors of custom Gutenberg integrations. + +The purpose of this document is to present a picture of how many private APIs we have and how they are used to build the Gutenberg editor apps with the libraries and frameworks provided by the family of `@wordpress/*` packages. + +## data + +The registry has two private methods: +- `privateActionsOf` +- `privateSelectorsOf` + +Every store has a private API for registering private selectors/actions: +- `privateActions` +- `registerPrivateActions` +- `privateSelectors` +- `registerPrivateSelectors` + +## blocks + +### `core/blocks` store + +Private actions: +- `addBlockBindingsSource` +- `removeBlockBindingsSource` +- `addBootstrappedBlockType` +- `addUnprocessedBlockType` + +Private selectors: +- `getAllBlockBindingsSources` +- `getBlockBindingsSource` +- `getBootstrappedBlockType` +- `getSupportedStyles` +- `getUnprocessedBlockTypes` +- `hasContentRoleAttribute` + +## components + +Private exports: +- `__experimentalPopoverLegacyPositionToPlacement` +- `ComponentsContext` +- `Tabs` +- `Theme` +- `Menu` +- `kebabCase` + +## commands + +Private exports: +- `useCommandContext` (added May 2023 in #50543) + +### `core/commands` store + +Private actions: +- `setContext` (added together with `useCommandContext`) + +## preferences + +Private exports: (added in Jan 2024 in #57639) +- `PreferenceBaseOption` +- `PreferenceToggleControl` +- `PreferencesModal` +- `PreferencesModalSection` +- `PreferencesModalTabs` + +There is only one publicly exported component! +- `PreferenceToggleMenuItem` + +## block-editor + +Private exports: +- `AdvancedPanel` +- `BackgroundPanel` +- `BorderPanel` +- `ColorPanel` +- `DimensionsPanel` +- `FiltersPanel` +- `GlobalStylesContext` +- `ImageSettingsPanel` +- `TypographyPanel` +- `areGlobalStyleConfigsEqual` +- `getBlockCSSSelector` +- `getBlockSelectors` +- `getGlobalStylesChanges` +- `getLayoutStyles` +- `toStyles` +- `useGlobalSetting` +- `useGlobalStyle` +- `useGlobalStylesOutput` +- `useGlobalStylesOutputWithConfig` +- `useGlobalStylesReset` +- `useHasBackgroundPanel` +- `useHasBorderPanel` +- `useHasBorderPanelControls` +- `useHasColorPanel` +- `useHasDimensionsPanel` +- `useHasFiltersPanel` +- `useHasImageSettingsPanel` +- `useHasTypographyPanel` +- `useSettingsForBlockElement` +- `ExperimentalBlockCanvas`: version of public `BlockCanvas` that has several extra props: `contentRef`, `shouldIframe`, `iframeProps`. +- `ExperimentalBlockEditorProvider`: version of public `BlockEditorProvider` that filters out several private/experimental settings. See also `__experimentalUpdateSettings`. +- `getDuotoneFilter` +- `getRichTextValues` +- `PrivateQuickInserter` +- `extractWords` +- `getNormalizedSearchTerms` +- `normalizeString` +- `PrivateListView` +- `ResizableBoxPopover` +- `BlockInfo` +- `useHasBlockToolbar` +- `cleanEmptyObject` +- `BlockQuickNavigation` +- `LayoutStyle` +- `BlockRemovalWarningModal` +- `useLayoutClasses` +- `useLayoutStyles` +- `DimensionsTool` +- `ResolutionTool` +- `TabbedSidebar` +- `TextAlignmentControl` +- `usesContextKey` +- `useFlashEditableBlocks` +- `useZoomOut` +- `globalStylesDataKey` +- `globalStylesLinksDataKey` +- `selectBlockPatternsKey` +- `requiresWrapperOnCopy` +- `PrivateRichText`: has an extra prop `readOnly` added in #58916 and #60327 (Feb and Mar 2024). +- `PrivateInserterLibrary`: has an extra prop `onPatternCategorySelection` added in #62130 (May 2024). +- `reusableBlocksSelectKey` +- `PrivateBlockPopover`: has two extra props, `__unstableContentRef` and `__unstablePopoverSlot`. +- `PrivatePublishDateTimePicker`: version of public `PublishDateTimePicker` that has two extra props: `isCompact` and `showPopoverHeaderActions`. +- `useSpacingSizes` +- `useBlockDisplayTitle` +- `__unstableBlockStyleVariationOverridesWithConfig` +- `setBackgroundStyleDefaults` +- `sectionRootClientIdKey` +- `__unstableCommentIconFill` +- `__unstableCommentIconToolbarFill` + +### `core/block-editor` store + +Private actions: +- `__experimentalUpdateSettings`: version of public `updateSettings` action that filters out some private/experimental settings. +- `clearBlockRemovalPrompt` +- `deleteStyleOverride` +- `ensureDefaultBlock` +- `expandBlock` +- `hideBlockInterface` +- `modifyContentLockBlock` +- `privateRemoveBlocks` +- `resetZoomLevel` +- `setBlockRemovalRules` +- `setInsertionPoint` +- `setLastFocus` +- `setOpenedBlockSettingsMenu` +- `setStyleOverride` +- `setZoomLevel` +- `showBlockInterface` +- `startDragging` +- `stopDragging` +- `stopEditingAsBlocks` + +Private selectors: +- `getAllPatterns` +- `getBlockRemovalRules` +- `getBlockSettings` +- `getBlockStyles` +- `getBlockWithoutAttributes` +- `getClosestAllowedInsertionPoint` +- `getClosestAllowedInsertionPointForPattern` +- `getContentLockingParent` +- `getEnabledBlockParents` +- `getEnabledClientIdsTree` +- `getExpandedBlock` +- `getInserterMediaCategories` +- `getInsertionPoint` +- `getLastFocus` +- `getLastInsertedBlocksClientIds` +- `getOpenedBlockSettingsMenu` +- `getParentSectionBlock` +- `getPatternBySlug` +- `getRegisteredInserterMediaCategories` +- `getRemovalPromptData` +- `getReusableBlocks` +- `getSectionRootClientId` +- `getStyleOverrides` +- `getTemporarilyEditingAsBlocks` +- `getTemporarilyEditingFocusModeToRevert` +- `getZoomLevel` +- `hasAllowedPatterns` +- `isBlockInterfaceHidden` +- `isBlockSubtreeDisabled` +- `isDragging` +- `isResolvingPatterns` +- `isSectionBlock` +- `isZoomOut` + +## core-data + +Private exports: +- `useEntityRecordsWithPermissions` + +### `core` store + +Private actions: +- `receiveRegisteredPostMeta` + +Private selectors: +- `getBlockPatternsForPostType` +- `getEntityRecordPermissions` +- `getEntityRecordsPermissions` +- `getNavigationFallbackId` +- `getRegisteredPostMeta` +- `getUndoManager` + +## patterns (package created in Aug 2023 and has no public exports, everything is private) + +Private exports: +- `OverridesPanel` +- `CreatePatternModal` +- `CreatePatternModalContents` +- `DuplicatePatternModal` +- `isOverridableBlock` +- `hasOverridableBlocks` +- `useDuplicatePatternProps` +- `RenamePatternModal` +- `PatternsMenuItems` +- `RenamePatternCategoryModal` +- `PatternOverridesControls` +- `ResetOverridesControl` +- `PatternOverridesBlockControls` +- `useAddPatternCategory` +- `PATTERN_TYPES` +- `PATTERN_DEFAULT_CATEGORY` +- `PATTERN_USER_CATEGORY` +- `EXCLUDED_PATTERN_SOURCES` +- `PATTERN_SYNC_TYPES` +- `PARTIAL_SYNCING_SUPPORTED_BLOCKS` + +### `core/patterns` store + +Private actions: +- `convertSyncedPatternToStatic` +- `createPattern` +- `createPatternFromFile` +- `setEditingPattern` + +Private selectors: +- `isEditingPattern` + +## block-library + +Private exports: +- `BlockKeyboardShortcuts` + +## router (private exports only) + +Private exports: +- `useHistory` +- `useLocation` +- `RouterProvider` + +## core-commands (private exports only) + +Private exports: +- `useCommands` + +## editor + +Private exports: +- `CreateTemplatePartModal` +- `BackButton` +- `EntitiesSavedStatesExtensible` +- `Editor` +- `EditorContentSlotFill` +- `GlobalStylesProvider` +- `mergeBaseAndUserConfigs` +- `PluginPostExcerpt` +- `PostCardPanel` +- `PreferencesModal` +- `usePostActions` +- `ToolsMoreMenuGroup` +- `ViewMoreMenuGroup` +- `ResizableEditor` +- `registerCoreBlockBindingsSources` +- `interfaceStore` +- `ActionItem` +- `ComplementaryArea` +- `ComplementaryAreaMoreMenuItem` +- `FullscreenMode` +- `InterfaceSkeleton` +- `NavigableRegion` +- `PinnedItems` + +### `core/editor` store + +Private actions: +- `createTemplate` +- `hideBlockTypes` +- `registerEntityAction` +- `registerPostTypeActions` +- `removeTemplates` +- `revertTemplate` +- `saveDirtyEntities` +- `setCurrentTemplateId` +- `setIsReady` +- `showBlockTypes` +- `unregisterEntityAction` + +Private selectors: +- `getEntityActions` +- `getInserter` +- `getInserterSidebarToggleRef` +- `getListViewToggleRef` +- `getPostBlocksByName` +- `getPostIcon` +- `hasPostMetaChanges` +- `isEntityReady` + +## edit-post + +### `core/edit-post` store + +Private selectors: +- `getEditedPostTemplateId` + +## edit-site + +### `core/edit-site` store + +Private actions: +- `registerRoute` +- `setEditorCanvasContainerView` + +Private selectors: +- `getRoutes` +- `getEditorCanvasContainerView` diff --git a/docs/reference-guides/block-api/block-edit-save.md b/docs/reference-guides/block-api/block-edit-save.md index 86721c77e463c6..a50a17b75cb54d 100644 --- a/docs/reference-guides/block-api/block-edit-save.md +++ b/docs/reference-guides/block-api/block-edit-save.md @@ -183,9 +183,34 @@ save: ( { attributes } ) => { ``` - When saving your block, you want to save the attributes in the same format specified by the attribute source definition. If no attribute source is specified, the attribute will be saved to the block's comment delimiter. See the [Block Attributes documentation](/docs/reference-guides/block-api/block-attributes.md) for more details. +### innerBlocks + +There is a second property in the props passed to the `save` function, `innerBlocks`. This property is typically used for internal operations, and there are very few scenarios where you would need to use it. + +`innerBlocks`, when initialized, is an array containing object representations of nested blocks. In those rare cases where you might use this property, +it can help you adjust how a block is rendered. For example, you could render a block differently based on the number of nested blocks or if a specific block type is present.. + + +```jsx +save: ( { attributes, innerBlocks } ) => { + const { className, ...rest } = useBlockProps.save(); + + // innerBlocks could also be an object - react element during initialization + const numberOfInnerBlocks = innerBlocks?.length; + if ( numberOfInnerBlocks > 1 ) { + className = className + ( className ? ' ' : '' ) + 'more-than-one'; + }; + const blockProps = { ...rest, className }; + + return
{ attributes.content }
; +}; +``` + + +Here, an additional class is added to the block if number of inner blocks is greater than one, allowing for different styling of the block. + ## Examples Here are a couple examples of using attributes, edit, and save all together. diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 7e031fa525e1ff..0715b1e3547e2a 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -661,7 +661,7 @@ Contains the block elements used to render a post, like the title, date, feature - **Name:** core/post-template - **Category:** theme - **Ancestor:** core/query -- **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), layout, spacing (blockGap), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), layout, spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ ## Post Terms diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 437f7be20f7705..bca05d57610934 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -190,7 +190,7 @@ _Parameters_ _Returns_ -- `Object?`: Block attributes. +- `?Object`: Block attributes. ### getBlockCount @@ -448,7 +448,7 @@ Determines the items that appear in the available block transforms list. Each item object contains what's necessary to display a menu item in the transform list and handle its selection. -The 'frecency' property is a heuristic () that combines block usage frequenty and recency. +The 'frecency' property is a heuristic () that combines block usage frequency and recency. Items are returned ordered descendingly by their 'frecency'. @@ -521,7 +521,7 @@ _Properties_ - _name_ `string`: The type of block. - _attributes_ `?Object`: Attributes to pass to the newly created block. -- _attributesToCopy_ `?Array`: Attributes to be copied from adjecent blocks when inserted. +- _attributesToCopy_ `?Array`: Attributes to be copied from adjacent blocks when inserted. ### getDraggedBlockClientIds @@ -580,7 +580,7 @@ Determines the items that appear in the inserter. Includes both static items (e. Each item object contains what's necessary to display a button in the inserter and handle its selection. -The 'frecency' property is a heuristic () that combines block usage frequenty and recency. +The 'frecency' property is a heuristic () that combines block usage frequency and recency. Items are returned ordered descendingly by their 'utility' and 'frecency'. diff --git a/docs/reference-guides/data/data-core-blocks.md b/docs/reference-guides/data/data-core-blocks.md index 158b7f92529122..04292135aca51b 100644 --- a/docs/reference-guides/data/data-core-blocks.md +++ b/docs/reference-guides/data/data-core-blocks.md @@ -172,7 +172,7 @@ _Parameters_ _Returns_ -- `Object?`: Block Type. +- `?Object`: Block Type. ### getBlockTypes diff --git a/docs/reference-guides/data/data-core-edit-post.md b/docs/reference-guides/data/data-core-edit-post.md index 06fe5fc30420ae..c316a9266af98a 100644 --- a/docs/reference-guides/data/data-core-edit-post.md +++ b/docs/reference-guides/data/data-core-edit-post.md @@ -65,7 +65,7 @@ Retrieves the template of the currently edited post. _Returns_ -- `Object?`: Post Template. +- `?Object`: Post Template. ### getEditorMode diff --git a/docs/reference-guides/data/data-core-rich-text.md b/docs/reference-guides/data/data-core-rich-text.md index 55220b3ca9c5d9..8c213ee9c69ec4 100644 --- a/docs/reference-guides/data/data-core-rich-text.md +++ b/docs/reference-guides/data/data-core-rich-text.md @@ -46,7 +46,7 @@ _Parameters_ _Returns_ -- `Object?`: Format type. +- `?Object`: Format type. ### getFormatTypeForBareElement diff --git a/docs/tool/manifest.js b/docs/tool/manifest.js index 2004fae84f7ccc..569d78bc5bea8a 100644 --- a/docs/tool/manifest.js +++ b/docs/tool/manifest.js @@ -18,6 +18,7 @@ const componentPaths = glob( 'packages/components/src/*/**/README.md', { 'packages/components/src/menu/README.md', 'packages/components/src/tabs/README.md', 'packages/components/src/custom-select-control-v2/README.md', + 'packages/components/src/badge/README.md', ], } ); const packagePaths = glob( 'packages/*/package.json' ) diff --git a/gutenberg.php b/gutenberg.php index 92f935669fc46e..f736359a8b357b 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.6 * Requires PHP: 7.2 - * Version: 19.9.0-rc.1 + * Version: 20.0.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 204c9955c3cff1..00000000000000 --- a/jsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@wordpress/*": [ "./*", "./packages/*/src" ] - } - }, - "exclude": [ - "build", - "build-module", - "node_modules", - "packages/e2e-tests/plugins", - "vendor" - ] -} diff --git a/lib/block-supports/block-style-variations.php b/lib/block-supports/block-style-variations.php index 3942fed24b98a8..5f7d02007ed396 100644 --- a/lib/block-supports/block-style-variations.php +++ b/lib/block-supports/block-style-variations.php @@ -211,10 +211,10 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block ) * block attributes in the `render_block_data` filter gets applied to the * block's markup. * - * @see gutenberg_render_block_style_variation_support_styles - * * @since 6.6.0 * + * @see gutenberg_render_block_style_variation_support_styles + * * @param string $block_content Rendered block content. * @param array $block Block object. * @@ -273,7 +273,7 @@ function gutenberg_enqueue_block_style_variation_styles() { } // Add Gutenberg filters and action. -add_filter( 'render_block_data', 'gutenberg_render_block_style_variation_support_styles', 10, 2 ); +add_filter( 'render_block_data', 'gutenberg_render_block_style_variation_support_styles' ); add_filter( 'render_block', 'gutenberg_render_block_style_variation_class_name', 10, 2 ); add_action( 'wp_enqueue_scripts', 'gutenberg_enqueue_block_style_variation_styles', 1 ); diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php index f890ed84566b7f..bd4c772675a5ed 100644 --- a/lib/block-supports/border.php +++ b/lib/block-supports/border.php @@ -17,7 +17,7 @@ function gutenberg_register_border_support( $block_type ) { $block_type->attributes = array(); } - if ( block_has_support( $block_type, array( 'border' ) ) && ! array_key_exists( 'style', $block_type->attributes ) ) { + if ( block_has_support( $block_type, array( '__experimentalBorder' ) ) && ! array_key_exists( 'style', $block_type->attributes ) ) { $block_type->attributes['style'] = array( 'type' => 'object', ); @@ -52,7 +52,7 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { if ( gutenberg_has_border_feature_support( $block_type, 'radius' ) && isset( $block_attributes['style']['border']['radius'] ) && - ! wp_should_skip_block_supports_serialization( $block_type, 'border', 'radius' ) + ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'radius' ) ) { $border_radius = $block_attributes['style']['border']['radius']; @@ -67,7 +67,7 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { if ( gutenberg_has_border_feature_support( $block_type, 'style' ) && isset( $block_attributes['style']['border']['style'] ) && - ! wp_should_skip_block_supports_serialization( $block_type, 'border', 'style' ) + ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'style' ) ) { $border_block_styles['style'] = $block_attributes['style']['border']['style']; } @@ -76,7 +76,7 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { if ( $has_border_width_support && isset( $block_attributes['style']['border']['width'] ) && - ! wp_should_skip_block_supports_serialization( $block_type, 'border', 'width' ) + ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'width' ) ) { $border_width = $block_attributes['style']['border']['width']; @@ -91,7 +91,7 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { // Border color. if ( $has_border_color_support && - ! wp_should_skip_block_supports_serialization( $block_type, 'border', 'color' ) + ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'color' ) ) { $preset_border_color = array_key_exists( 'borderColor', $block_attributes ) ? "var:preset|color|{$block_attributes['borderColor']}" : null; $custom_border_color = $block_attributes['style']['border']['color'] ?? null; @@ -103,9 +103,9 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) { $border = $block_attributes['style']['border'][ $side ] ?? null; $border_side_values = array( - 'width' => isset( $border['width'] ) && ! wp_should_skip_block_supports_serialization( $block_type, 'border', 'width' ) ? $border['width'] : null, - 'color' => isset( $border['color'] ) && ! wp_should_skip_block_supports_serialization( $block_type, 'border', 'color' ) ? $border['color'] : null, - 'style' => isset( $border['style'] ) && ! wp_should_skip_block_supports_serialization( $block_type, 'border', 'style' ) ? $border['style'] : null, + 'width' => isset( $border['width'] ) && ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'width' ) ? $border['width'] : null, + 'color' => isset( $border['color'] ) && ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'color' ) ? $border['color'] : null, + 'style' => isset( $border['style'] ) && ! wp_should_skip_block_supports_serialization( $block_type, '__experimentalBorder', 'style' ) ? $border['style'] : null, ); $border_block_styles[ $side ] = $border_side_values; } @@ -129,9 +129,9 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { /** * Checks whether the current block type supports the border feature requested. * - * If the `border` support flag is a boolean `true` all border + * If the `__experimentalBorder` support flag is a boolean `true` all border * support features are available. Otherwise, the specific feature's support - * flag nested under `border` must be enabled for the feature + * flag nested under `experimentalBorder` must be enabled for the feature * to be opted into. * * @param WP_Block_Type $block_type Block type to check for support. @@ -141,17 +141,17 @@ function gutenberg_apply_border_support( $block_type, $block_attributes ) { * @return boolean Whether or not the feature is supported. */ function gutenberg_has_border_feature_support( $block_type, $feature, $default_value = false ) { - // Check if all border support features have been opted into via `"border": true`. + // Check if all border support features have been opted into via `"__experimentalBorder": true`. if ( $block_type instanceof WP_Block_Type ) { - $block_type_supports_border = $block_type->supports['border'] ?? $default_value; + $block_type_supports_border = $block_type->supports['__experimentalBorder'] ?? $default_value; if ( true === $block_type_supports_border ) { return true; } } // Check if the specific feature has been opted into individually - // via nested flag under `border`. - return block_has_support( $block_type, array( 'border', $feature ), $default_value ); + // via nested flag under `__experimentalBorder`. + return block_has_support( $block_type, array( '__experimentalBorder', $feature ), $default_value ); } // Register the block support. diff --git a/lib/block-supports/elements.php b/lib/block-supports/elements.php index 35a41270a19800..f3243bc7178951 100644 --- a/lib/block-supports/elements.php +++ b/lib/block-supports/elements.php @@ -255,12 +255,12 @@ function gutenberg_render_elements_class_name( $block_content, $block ) { } // Remove deprecated WordPress core filters. -remove_filter( 'render_block', 'wp_render_elements_support', 10, 2 ); -remove_filter( 'pre_render_block', 'wp_render_elements_support_styles', 10, 2 ); +remove_filter( 'render_block', 'wp_render_elements_support', 10 ); +remove_filter( 'pre_render_block', 'wp_render_elements_support_styles', 10 ); // Remove WordPress core filters to avoid rendering duplicate elements stylesheet & attaching classes twice. -remove_filter( 'render_block', 'wp_render_elements_class_name', 10, 2 ); -remove_filter( 'render_block_data', 'wp_render_elements_support_styles', 10, 1 ); +remove_filter( 'render_block', 'wp_render_elements_class_name', 10 ); +remove_filter( 'render_block_data', 'wp_render_elements_support_styles', 10 ); add_filter( 'render_block', 'gutenberg_render_elements_class_name', 10, 2 ); add_filter( 'render_block_data', 'gutenberg_render_elements_support_styles', 10, 1 ); diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index ddbd1917c30547..7d63074ccb09bb 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -1055,8 +1055,8 @@ static function ( $matches ) { } if ( function_exists( 'wp_restore_group_inner_container' ) ) { - remove_filter( 'render_block', 'wp_restore_group_inner_container', 10, 2 ); - remove_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10, 2 ); + remove_filter( 'render_block', 'wp_restore_group_inner_container', 10 ); + remove_filter( 'render_block_core/group', 'wp_restore_group_inner_container', 10 ); } add_filter( 'render_block_core/group', 'gutenberg_restore_group_inner_container', 10, 2 ); @@ -1118,6 +1118,6 @@ function gutenberg_restore_image_outer_container( $block_content, $block ) { } if ( function_exists( 'wp_restore_image_outer_container' ) ) { - remove_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10, 2 ); + remove_filter( 'render_block_core/image', 'wp_restore_image_outer_container', 10 ); } add_filter( 'render_block_core/image', 'gutenberg_restore_image_outer_container', 10, 2 ); diff --git a/lib/block-supports/settings.php b/lib/block-supports/settings.php index b175fe778ce1b0..0246b5c039c86a 100644 --- a/lib/block-supports/settings.php +++ b/lib/block-supports/settings.php @@ -128,7 +128,7 @@ function _gutenberg_add_block_level_preset_styles( $pre_render, $block ) { return null; } // Remove WordPress core filter to avoid rendering duplicate settings style blocks. -remove_filter( 'render_block', '_wp_add_block_level_presets_class', 10, 2 ); -remove_filter( 'pre_render_block', '_wp_add_block_level_preset_styles', 10, 2 ); +remove_filter( 'render_block', '_wp_add_block_level_presets_class', 10 ); +remove_filter( 'pre_render_block', '_wp_add_block_level_preset_styles', 10 ); add_filter( 'render_block', '_gutenberg_add_block_level_presets_class', 10, 2 ); add_filter( 'pre_render_block', '_gutenberg_add_block_level_preset_styles', 10, 2 ); diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index 21086b94f15c1a..a4719b7bdd4099 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -20,16 +20,16 @@ function gutenberg_register_typography_support( $block_type ) { return; } - $has_font_family_support = $typography_supports['fontFamily'] ?? false; + $has_font_family_support = $typography_supports['__experimentalFontFamily'] ?? false; $has_font_size_support = $typography_supports['fontSize'] ?? false; - $has_font_style_support = $typography_supports['fontStyle'] ?? false; - $has_font_weight_support = $typography_supports['fontWeight'] ?? false; - $has_letter_spacing_support = $typography_supports['letterSpacing'] ?? false; + $has_font_style_support = $typography_supports['__experimentalFontStyle'] ?? false; + $has_font_weight_support = $typography_supports['__experimentalFontWeight'] ?? false; + $has_letter_spacing_support = $typography_supports['__experimentalLetterSpacing'] ?? false; $has_line_height_support = $typography_supports['lineHeight'] ?? false; $has_text_align_support = $typography_supports['textAlign'] ?? false; $has_text_columns_support = $typography_supports['textColumns'] ?? false; - $has_text_decoration_support = $typography_supports['textDecoration'] ?? false; - $has_text_transform_support = $typography_supports['textTransform'] ?? false; + $has_text_decoration_support = $typography_supports['__experimentalTextDecoration'] ?? false; + $has_text_transform_support = $typography_supports['__experimentalTextTransform'] ?? false; $has_writing_mode_support = $typography_supports['__experimentalWritingMode'] ?? false; $has_typography_support = $has_font_family_support @@ -91,16 +91,16 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { return array(); } - $has_font_family_support = $typography_supports['fontFamily'] ?? false; + $has_font_family_support = $typography_supports['__experimentalFontFamily'] ?? false; $has_font_size_support = $typography_supports['fontSize'] ?? false; - $has_font_style_support = $typography_supports['fontStyle'] ?? false; - $has_font_weight_support = $typography_supports['fontWeight'] ?? false; - $has_letter_spacing_support = $typography_supports['letterSpacing'] ?? false; + $has_font_style_support = $typography_supports['__experimentalFontStyle'] ?? false; + $has_font_weight_support = $typography_supports['__experimentalFontWeight'] ?? false; + $has_letter_spacing_support = $typography_supports['__experimentalLetterSpacing'] ?? false; $has_line_height_support = $typography_supports['lineHeight'] ?? false; $has_text_align_support = $typography_supports['textAlign'] ?? false; $has_text_columns_support = $typography_supports['textColumns'] ?? false; - $has_text_decoration_support = $typography_supports['textDecoration'] ?? false; - $has_text_transform_support = $typography_supports['textTransform'] ?? false; + $has_text_decoration_support = $typography_supports['__experimentalTextDecoration'] ?? false; + $has_text_transform_support = $typography_supports['__experimentalTextTransform'] ?? false; $has_writing_mode_support = $typography_supports['__experimentalWritingMode'] ?? false; // Whether to skip individual block support features. diff --git a/lib/class-wp-duotone-gutenberg.php b/lib/class-wp-duotone-gutenberg.php index 5f3b1bb5cd6b11..cc49c320da6506 100644 --- a/lib/class-wp-duotone-gutenberg.php +++ b/lib/class-wp-duotone-gutenberg.php @@ -640,7 +640,7 @@ private static function get_global_styles_presets( $sources ) { * * @param string $block_name The block name. * - * @return string The CSS selector or null if there is no support. + * @return ?string The CSS selector or null if there is no support. */ private static function get_selector( $block_name ) { $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); @@ -669,6 +669,8 @@ private static function get_selector( $block_name ) { // Regular filter.duotone support uses filter.duotone selectors with fallbacks. return wp_get_block_css_selector( $block_type, array( 'filter', 'duotone' ), true ); } + + return null; } /** diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 778dcdbec78d96..3af123d96bcc5a 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -615,10 +615,10 @@ class WP_Theme_JSON_Gutenberg { * @var string[] */ const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array( - 'border' => 'border', - 'color' => 'color', - 'spacing' => 'spacing', - 'typography' => 'typography', + '__experimentalBorder' => 'border', + 'color' => 'color', + 'spacing' => 'spacing', + 'typography' => 'typography', ); /** @@ -3413,6 +3413,8 @@ protected static function should_override_preset( $theme_json, $path, $override return true; } + + return false; } /** diff --git a/lib/compat/wordpress-6.8/blocks.php b/lib/compat/wordpress-6.8/blocks.php index e0f5082bfce8dc..6cfa98691020ef 100644 --- a/lib/compat/wordpress-6.8/blocks.php +++ b/lib/compat/wordpress-6.8/blocks.php @@ -5,147 +5,175 @@ * @package gutenberg */ +function gutenberg_apply_block_hooks_to_post_content( $content ) { + // The `the_content` filter does not provide the post that the content is coming from. + // However, we can infer it by calling `get_post()`, which will return the current post + // if no post ID is provided. + return apply_block_hooks_to_content( $content, get_post(), 'insert_hooked_blocks' ); +} +// We need to apply this filter before `do_blocks` (which is hooked to `the_content` at priority 9). +add_filter( 'the_content', 'gutenberg_apply_block_hooks_to_post_content', 8 ); + /** - * Filters the block type arguments during registration to stabilize - * experimental block supports. + * Hooks into the REST API response for the Posts endpoint and adds the first and last inner blocks. * - * This is a temporary compatibility shim as the approach in core is for this - * to be handled within the WP_Block_Type class rather than requiring a filter. + * @since 6.6.0 + * @since 6.8.0 Support non-`wp_navigation` post types. * - * @param array $args Array of arguments for registering a block type. - * @return array Array of arguments for registering a block type. + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @return WP_REST_Response The response object. */ -function gutenberg_stabilize_experimental_block_supports( $args ) { - if ( empty( $args['supports'] ) ) { - return $args; +function gutenberg_insert_hooked_blocks_into_rest_response( $response, $post ) { + if ( empty( $response->data['content']['raw'] ) ) { + return $response; + } + + $attributes = array(); + $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $ignored_hooked_blocks ) ) { + $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); + $attributes['metadata'] = array( + 'ignoredHookedBlocks' => $ignored_hooked_blocks, + ); + } + + if ( 'wp_navigation' === $post->post_type ) { + $wrapper_block_type = 'core/navigation'; + } elseif ( 'wp_block' === $post->post_type ) { + $wrapper_block_type = 'core/block'; + } else { + $wrapper_block_type = 'core/post-content'; } - $experimental_supports_map = array( '__experimentalBorder' => 'border' ); - $common_experimental_properties = array( - '__experimentalDefaultControls' => 'defaultControls', - '__experimentalSkipSerialization' => 'skipSerialization', + $content = get_comment_delimited_block_content( + $wrapper_block_type, + $attributes, + $response->data['content']['raw'] ); - $experimental_support_properties = array( - 'typography' => array( - '__experimentalFontFamily' => 'fontFamily', - '__experimentalFontStyle' => 'fontStyle', - '__experimentalFontWeight' => 'fontWeight', - '__experimentalLetterSpacing' => 'letterSpacing', - '__experimentalTextDecoration' => 'textDecoration', - '__experimentalTextTransform' => 'textTransform', - ), + + $content = apply_block_hooks_to_content( + $content, + $post, + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata' ); - $done = array(); - - $updated_supports = array(); - foreach ( $args['supports'] as $support => $config ) { - /* - * If this support config has already been stabilized, skip it. - * A stable support key occurring after an experimental key, gets - * stabilized then so that the two configs can be merged effectively. - */ - if ( isset( $done[ $support ] ) ) { - continue; - } - $stable_support_key = $experimental_supports_map[ $support ] ?? $support; - - /* - * Use the support's config as is when it's not in need of stabilization. - * - * A support does not need stabilization if: - * - The support key doesn't need stabilization AND - * - Either: - * - The config isn't an array, so can't have experimental properties OR - * - The config is an array but has no experimental properties to stabilize. - */ - if ( $support === $stable_support_key && - ( ! is_array( $config ) || - ( ! isset( $experimental_support_properties[ $stable_support_key ] ) && - empty( array_intersect_key( $common_experimental_properties, $config ) ) - ) - ) - ) { - $updated_supports[ $support ] = $config; - continue; - } + // Remove mock block wrapper. + $content = remove_serialized_parent_block( $content ); - $stabilize_config = function ( $unstable_config, $stable_support_key ) use ( $experimental_support_properties, $common_experimental_properties ) { - if ( ! is_array( $unstable_config ) ) { - return $unstable_config; - } - - $stable_config = array(); - foreach ( $unstable_config as $key => $value ) { - // Get stable key from support-specific map, common properties map, or keep original. - $stable_key = $experimental_support_properties[ $stable_support_key ][ $key ] ?? - $common_experimental_properties[ $key ] ?? - $key; - - $stable_config[ $stable_key ] = $value; - - /* - * The `__experimentalSkipSerialization` key needs to be kept until - * WP 6.8 becomes the minimum supported version. This is due to the - * core `wp_should_skip_block_supports_serialization` function only - * checking for `__experimentalSkipSerialization` in earlier versions. - */ - if ( '__experimentalSkipSerialization' === $key || 'skipSerialization' === $key ) { - $stable_config['__experimentalSkipSerialization'] = $value; - } - } - return $stable_config; - }; - - // Stabilize the config value. - $stable_config = is_array( $config ) ? $stabilize_config( $config, $stable_support_key ) : $config; - - /* - * If a plugin overrides the support config with the `register_block_type_args` - * filter, both experimental and stable configs may be present. In that case, - * use the order keys are defined in to determine the final value. - * - If config is an array, merge the arrays in their order of definition. - * - If config is not an array, use the value defined last. - * - * The reason for preferring the last defined key is that after filters - * are applied, the last inserted key is likely the most up-to-date value. - * We cannot determine with certainty which value was "last modified" so - * the insertion order is the best guess. The extreme edge case of multiple - * filters tweaking the same support property will become less over time as - * extenders migrate existing blocks and plugins to stable keys. - */ - if ( $support !== $stable_support_key && isset( $args['supports'][ $stable_support_key ] ) ) { - $key_positions = array_flip( array_keys( $args['supports'] ) ); - $experimental_first = - ( $key_positions[ $support ] ?? PHP_INT_MAX ) < - ( $key_positions[ $stable_support_key ] ?? PHP_INT_MAX ); - - /* - * To merge the alternative support config effectively, it also needs to be - * stabilized before merging to keep stabilized and experimental flags in - * sync. - */ - $args['supports'][ $stable_support_key ] = $stabilize_config( $args['supports'][ $stable_support_key ], $stable_support_key ); - // Prevents reprocessing this support as it was stabilized above. - $done[ $stable_support_key ] = true; - - if ( is_array( $stable_config ) && is_array( $args['supports'][ $stable_support_key ] ) ) { - $stable_config = $experimental_first - ? array_merge( $stable_config, $args['supports'][ $stable_support_key ] ) - : array_merge( $args['supports'][ $stable_support_key ], $stable_config ); - } else { - $stable_config = $experimental_first - ? $args['supports'][ $stable_support_key ] - : $stable_config; - } - } + $response->data['content']['raw'] = $content; - $updated_supports[ $stable_support_key ] = $stable_config; + // If the rendered content was previously empty, we leave it like that. + if ( empty( $response->data['content']['rendered'] ) ) { + return $response; } - $args['supports'] = $updated_supports; + // No need to inject hooked blocks twice. + $priority = has_filter( 'the_content', 'apply_block_hooks_to_content' ); + if ( false !== $priority ) { + remove_filter( 'the_content', 'apply_block_hooks_to_content', $priority ); + } + + /** This filter is documented in wp-includes/post-template.php */ + $response->data['content']['rendered'] = apply_filters( 'the_content', $content ); - return $args; + // Add back the filter. + if ( false !== $priority ) { + add_filter( 'the_content', 'apply_block_hooks_to_content', $priority ); + } + + return $response; } +add_filter( 'rest_prepare_page', 'gutenberg_insert_hooked_blocks_into_rest_response', 10, 2 ); +add_filter( 'rest_prepare_post', 'gutenberg_insert_hooked_blocks_into_rest_response', 10, 2 ); +add_filter( 'rest_prepare_wp_block', 'gutenberg_insert_hooked_blocks_into_rest_response', 10, 2 ); -add_filter( 'register_block_type_args', 'gutenberg_stabilize_experimental_block_supports', PHP_INT_MAX, 1 ); +/** + * Updates the wp_postmeta with the list of ignored hooked blocks + * where the inner blocks are stored as post content. + * + * @since 6.6.0 + * @since 6.8.0 Support other post types. (Previously, it was limited to `wp_navigation` only.) + * @access private + * + * @param stdClass $post Post object. + * @return stdClass The updated post object. + */ +function gutenberg_update_ignored_hooked_blocks_postmeta( $post ) { + /* + * In this scenario the user has likely tried to create a new post object via the REST API. + * In which case we won't have a post ID to work with and store meta against. + */ + if ( empty( $post->ID ) ) { + return $post; + } + + /* + * Skip meta generation when consumers intentionally update specific fields + * and omit the content update. + */ + if ( ! isset( $post->post_content ) ) { + return $post; + } + + /* + * Skip meta generation if post type is not set. + */ + if ( ! isset( $post->post_type ) ) { + return $post; + } + + $attributes = array(); + + $ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $ignored_hooked_blocks ) ) { + $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); + $attributes['metadata'] = array( + 'ignoredHookedBlocks' => $ignored_hooked_blocks, + ); + } + + if ( 'wp_navigation' === $post->post_type ) { + $wrapper_block_type = 'core/navigation'; + } elseif ( 'wp_block' === $post->post_type ) { + $wrapper_block_type = 'core/block'; + } else { + $wrapper_block_type = 'core/post-content'; + } + + $markup = get_comment_delimited_block_content( + $wrapper_block_type, + $attributes, + $post->post_content + ); + + $existing_post = get_post( $post->ID ); + // Merge the existing post object with the updated post object to pass to the block hooks algorithm for context. + $context = (object) array_merge( (array) $existing_post, (array) $post ); + $context = new WP_Post( $context ); // Convert to WP_Post object. + $serialized_block = apply_block_hooks_to_content( $markup, $context, 'set_ignored_hooked_blocks_metadata' ); + $root_block = parse_blocks( $serialized_block )[0]; + + $ignored_hooked_blocks = isset( $root_block['attrs']['metadata']['ignoredHookedBlocks'] ) + ? $root_block['attrs']['metadata']['ignoredHookedBlocks'] + : array(); + + if ( ! empty( $ignored_hooked_blocks ) ) { + $existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $existing_ignored_hooked_blocks ) ) { + $existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true ); + $ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) ); + } + + if ( ! isset( $post->meta_input ) ) { + $post->meta_input = array(); + } + $post->meta_input['_wp_ignored_hooked_blocks'] = json_encode( $ignored_hooked_blocks ); + } + + $post->post_content = remove_serialized_parent_block( $serialized_block ); + return $post; +} +add_filter( 'rest_pre_insert_page', 'gutenberg_update_ignored_hooked_blocks_postmeta' ); +add_filter( 'rest_pre_insert_post', 'gutenberg_update_ignored_hooked_blocks_postmeta' ); +add_filter( 'rest_pre_insert_wp_block', 'gutenberg_update_ignored_hooked_blocks_postmeta' ); diff --git a/lib/compat/wordpress-6.8/class-gutenberg-hierarchical-sort.php b/lib/compat/wordpress-6.8/class-gutenberg-hierarchical-sort.php new file mode 100644 index 00000000000000..f61002f435a760 --- /dev/null +++ b/lib/compat/wordpress-6.8/class-gutenberg-hierarchical-sort.php @@ -0,0 +1,205 @@ + 'id=>parent', + 'posts_per_page' => -1, + ) + ); + $query = new WP_Query( $new_args ); + $posts = $query->posts; + $result = self::sort( $posts ); + + self::$post_ids = $result['post_ids']; + self::$levels = $result['levels']; + } + + /** + * Check if the request is eligible for hierarchical sorting. + * + * @param array $request The request data. + * + * @return bool Return true if the request is eligible for hierarchical sorting. + */ + public static function is_eligible( $request ) { + if ( ! isset( $request['orderby_hierarchy'] ) || true !== $request['orderby_hierarchy'] ) { + return false; + } + + return true; + } + + public static function get_ancestor( $post_id ) { + return get_post( $post_id )->post_parent ?? 0; + } + + /** + * Sort posts by hierarchy. + * + * Takes an array of posts and sorts them based on their parent-child relationships. + * It also tracks the level depth of each post in the hierarchy. + * + * Example input: + * ``` + * [ + * ['ID' => 4, 'post_parent' => 2], + * ['ID' => 2, 'post_parent' => 0], + * ['ID' => 3, 'post_parent' => 2], + * ] + * ``` + * + * Example output: + * ``` + * [ + * 'post_ids' => [2, 4, 3], + * 'levels' => [0, 1, 1] + * ] + * ``` + * + * @param array $posts Array of post objects containing ID and post_parent properties. + * + * @return array { + * Sorted post IDs and their hierarchical levels + * + * @type array $post_ids Array of post IDs + * @type array $levels Array of levels for the corresponding post ID in the same index + * } + */ + public static function sort( $posts ) { + /* + * Arrange pages in two arrays: + * + * - $top_level: posts whose parent is 0 + * - $children: post ID as the key and an array of children post IDs as the value. + * Example: $children[10][] contains all sub-pages whose parent is 10. + * + * Additionally, keep track of the levels of each post in $levels. + * Example: $levels[10] = 0 means the post ID is a top-level page. + * + */ + $top_level = array(); + $children = array(); + foreach ( $posts as $post ) { + if ( empty( $post->post_parent ) ) { + $top_level[] = $post->ID; + } else { + $children[ $post->post_parent ][] = $post->ID; + } + } + + $ids = array(); + $levels = array(); + self::add_hierarchical_ids( $ids, $levels, 0, $top_level, $children ); + + // Process remaining children. + if ( ! empty( $children ) ) { + foreach ( $children as $parent_id => $child_ids ) { + $level = 0; + $ancestor = $parent_id; + while ( 0 !== $ancestor ) { + ++$level; + $ancestor = self::get_ancestor( $ancestor ); + } + self::add_hierarchical_ids( $ids, $levels, $level, $child_ids, $children ); + } + } + + return array( + 'post_ids' => $ids, + 'levels' => $levels, + ); + } + + private static function add_hierarchical_ids( &$ids, &$levels, $level, $to_process, $children ) { + foreach ( $to_process as $id ) { + if ( in_array( $id, $ids, true ) ) { + continue; + } + $ids[] = $id; + $levels[ $id ] = $level; + + if ( isset( $children[ $id ] ) ) { + self::add_hierarchical_ids( $ids, $levels, $level + 1, $children[ $id ], $children ); + unset( $children[ $id ] ); + } + } + } + + public static function get_post_ids() { + return self::$post_ids; + } + + public static function get_levels() { + return self::$levels; + } +} + +add_filter( + 'rest_page_collection_params', + function ( $params ) { + $params['orderby_hierarchy'] = array( + 'description' => 'Sort pages by hierarchy.', + 'type' => 'boolean', + 'default' => false, + ); + return $params; + } +); + +add_filter( + 'rest_page_query', + function ( $args, $request ) { + if ( ! Gutenberg_Hierarchical_Sort::is_eligible( $request ) ) { + return $args; + } + + $hs = Gutenberg_Hierarchical_Sort::get_instance(); + $hs->run( $args ); + + // Reconfigure the args to display only the ids in the list. + $args['post__in'] = $hs->get_post_ids(); + $args['orderby'] = 'post__in'; + + return $args; + }, + 10, + 2 +); + +add_filter( + 'rest_prepare_page', + function ( $response, $post, $request ) { + if ( ! Gutenberg_Hierarchical_Sort::is_eligible( $request ) ) { + return $response; + } + + $hs = Gutenberg_Hierarchical_Sort::get_instance(); + $response->data['level'] = $hs->get_levels()[ $post->ID ]; + + return $response; + }, + 10, + 3 +); diff --git a/lib/compat/wordpress-6.8/post.php b/lib/compat/wordpress-6.8/post.php index 639e33b4e5ca51..be842d89b51519 100644 --- a/lib/compat/wordpress-6.8/post.php +++ b/lib/compat/wordpress-6.8/post.php @@ -32,15 +32,18 @@ function gutenberg_post_type_rendering_modes() { * @return array Updated array of post type arguments. */ function gutenberg_post_type_default_rendering_mode( $args, $post_type ) { - $rendering_mode = 'page' === $post_type ? 'template-locked' : 'post-only'; - $rendering_modes = gutenberg_post_type_rendering_modes(); + if ( ! wp_is_block_theme() || ! current_theme_supports( 'block-templates' ) ) { + return $args; + } // Make sure the post type supports the block editor. if ( - wp_is_block_theme() && ( isset( $args['show_in_rest'] ) && $args['show_in_rest'] ) && ( ! empty( $args['supports'] ) && in_array( 'editor', $args['supports'], true ) ) ) { + $rendering_mode = 'page' === $post_type ? 'template-locked' : 'post-only'; + $rendering_modes = gutenberg_post_type_rendering_modes(); + // Validate the supplied rendering mode. if ( isset( $args['default_rendering_mode'] ) && diff --git a/lib/compat/wordpress-6.8/site-editor.php b/lib/compat/wordpress-6.8/site-editor.php index 53d04c2e543f48..9b2575676047d1 100644 --- a/lib/compat/wordpress-6.8/site-editor.php +++ b/lib/compat/wordpress-6.8/site-editor.php @@ -145,4 +145,4 @@ function gutenberg_add_styles_submenu_item() { } } } -add_action( 'admin_init', 'gutenberg_add_styles_submenu_item' ); +add_action( 'admin_menu', 'gutenberg_add_styles_submenu_item' ); diff --git a/lib/experimental/font-face/bc-layer/webfonts-deprecations.php b/lib/experimental/font-face/bc-layer/webfonts-deprecations.php index 2534d8db165273..fb5e6b315dbdaf 100644 --- a/lib/experimental/font-face/bc-layer/webfonts-deprecations.php +++ b/lib/experimental/font-face/bc-layer/webfonts-deprecations.php @@ -28,7 +28,7 @@ function wp_webfonts() { global $wp_webfonts; if ( ! ( $wp_webfonts instanceof WP_Webfonts ) ) { - $wp_webfonts = new WP_Webfonts( wp_fonts() ); + $wp_webfonts = new WP_Webfonts(); } return $wp_webfonts; diff --git a/lib/experimental/kses-allowed-html.php b/lib/experimental/kses-allowed-html.php index 122faef7b4ca2c..9a4f2e7c614b80 100644 --- a/lib/experimental/kses-allowed-html.php +++ b/lib/experimental/kses-allowed-html.php @@ -40,4 +40,4 @@ function gutenberg_kses_allowed_html( $allowedtags ) { ); return $allowedtags; } -add_filter( 'wp_kses_allowed_html', 'gutenberg_kses_allowed_html', 10, 2 ); +add_filter( 'wp_kses_allowed_html', 'gutenberg_kses_allowed_html' ); diff --git a/lib/experimental/media/load.php b/lib/experimental/media/load.php index bcb02accf62a6b..18bfd22d4de00f 100644 --- a/lib/experimental/media/load.php +++ b/lib/experimental/media/load.php @@ -247,6 +247,8 @@ function gutenberg_set_up_cross_origin_isolation() { * Uses an output buffer to add crossorigin="anonymous" where needed. * * @link https://web.dev/coop-coep/ + * + * @global bool $is_safari */ function gutenberg_start_cross_origin_isolation_output_buffer(): void { global $is_safari; diff --git a/lib/experimental/posts/load.php b/lib/experimental/posts/load.php index 699534f1886f52..b6dd9d55a8d7d8 100644 --- a/lib/experimental/posts/load.php +++ b/lib/experimental/posts/load.php @@ -51,7 +51,7 @@ function gutenberg_posts_dashboard() { do_action( 'enqueue_block_editor_assets' ); wp_register_style( 'wp-gutenberg-posts-dashboard', - gutenberg_url( 'build/edit-site/posts.css', __FILE__ ), + gutenberg_url( 'build/edit-site/posts.css' ), array( 'wp-components', 'wp-commands', 'wp-edit-site' ) ); wp_enqueue_style( 'wp-gutenberg-posts-dashboard' ); diff --git a/lib/load.php b/lib/load.php index 26af78f3173c53..371f9c54e5fc4a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -45,6 +45,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.8/block-comments.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-comment-controller-6-8.php'; require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-post-types-controller-6-8.php'; + require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-hierarchical-sort.php'; require __DIR__ . '/compat/wordpress-6.8/rest-api.php'; // Plugin specific code. diff --git a/lib/rest-api.php b/lib/rest-api.php index 424927acf1f4a0..783abc24d3ee38 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -26,7 +26,7 @@ function gutenberg_override_global_styles_endpoint( array $args ): array { return $args; } -add_filter( 'register_wp_global_styles_post_type_args', 'gutenberg_override_global_styles_endpoint', 10, 2 ); +add_filter( 'register_wp_global_styles_post_type_args', 'gutenberg_override_global_styles_endpoint' ); /** * Registers the Edit Site Export REST API routes. diff --git a/lib/theme-i18n.json b/lib/theme-i18n.json index e4d14502132cbe..1b7a8d0d31190b 100644 --- a/lib/theme-i18n.json +++ b/lib/theme-i18n.json @@ -45,6 +45,13 @@ } ] }, + "shadow": { + "presets": [ + { + "name": "Shadow name" + } + ] + }, "blocks": { "*": { "typography": { @@ -69,6 +76,18 @@ { "name": "Gradient name" } + ], + "duotone": [ + { + "name": "Duotone name" + } + ] + }, + "dimensions": { + "aspectRatios": [ + { + "name": "Aspect ratio name" + } ] }, "spacing": { diff --git a/package-lock.json b/package-lock.json index 2dde4727531b6f..20c4950b3d732b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "19.9.0-rc.1", + "version": "20.0.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "19.9.0-rc.1", + "version": "20.0.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "workspaces": [ @@ -22,9 +22,11 @@ "@babel/runtime-corejs3": "7.25.7", "@babel/traverse": "7.25.7", "@emotion/babel-plugin": "11.11.0", + "@emotion/is-prop-valid": "1.2.2", "@emotion/jest": "11.7.1", "@emotion/native": "11.0.0", - "@geometricpanda/storybook-addon-badges": "2.0.1", + "@geometricpanda/storybook-addon-badges": "2.0.5", + "@inquirer/prompts": "7.2.0", "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.8.0", @@ -33,16 +35,19 @@ "@react-native/babel-preset": "0.73.10", "@react-native/metro-babel-transformer": "0.73.10", "@react-native/metro-config": "0.73.4", - "@storybook/addon-a11y": "7.6.15", - "@storybook/addon-actions": "7.6.15", - "@storybook/addon-controls": "7.6.15", - "@storybook/addon-docs": "7.6.15", - "@storybook/addon-toolbars": "7.6.15", - "@storybook/addon-viewport": "7.6.15", - "@storybook/react": "7.6.15", - "@storybook/react-webpack5": "7.6.15", - "@storybook/source-loader": "7.6.15", - "@storybook/theming": "7.6.15", + "@storybook/addon-a11y": "8.4.7", + "@storybook/addon-actions": "8.4.7", + "@storybook/addon-controls": "8.4.7", + "@storybook/addon-docs": "8.4.7", + "@storybook/addon-toolbars": "8.4.7", + "@storybook/addon-viewport": "8.4.7", + "@storybook/addon-webpack5-compiler-babel": "3.0.3", + "@storybook/react": "8.4.7", + "@storybook/react-webpack5": "8.4.7", + "@storybook/source-loader": "8.4.7", + "@storybook/test": "8.4.7", + "@storybook/theming": "8.4.7", + "@storybook/types": "8.4.7", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.3.0", "@testing-library/react-native": "12.4.3", @@ -51,6 +56,7 @@ "@types/estree": "1.0.5", "@types/istanbul-lib-report": "3.0.0", "@types/mime": "2.0.3", + "@types/node": "20.17.10", "@types/npm-package-arg": "6.1.1", "@types/prettier": "2.4.4", "@types/qs": "6.9.7", @@ -101,7 +107,6 @@ "filenamify": "4.2.0", "glob": "7.1.2", "husky": "7.0.0", - "inquirer": "7.1.0", "jest": "29.6.2", "jest-environment-jsdom": "^29.6.2", "jest-jasmine2": "29.6.2", @@ -139,15 +144,15 @@ "redux": "5.0.1", "resize-observer-polyfill": "1.5.1", "rimraf": "5.0.10", - "rtlcss": "4.0.0", - "sass": "1.50.1", + "rtlcss": "4.3.0", + "sass": "1.54.0", "sass-loader": "16.0.3", "semver": "7.5.4", "simple-git": "3.24.0", "snapshot-diff": "0.10.0", "source-map-loader": "3.0.0", "sprintf-js": "1.1.1", - "storybook": "7.6.15", + "storybook": "8.4.7", "storybook-source-link": "2.0.9", "strip-json-comments": "5.0.0", "style-loader": "3.2.1", @@ -1445,18 +1450,6 @@ } } }, - "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", - "integrity": "sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==", - "dev": true, - "dependencies": { - "default-browser-id": "3.0.0" - }, - "bin": { - "x-default-browser": "bin/x-default-browser.js" - } - }, "node_modules/@axe-core/puppeteer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@axe-core/puppeteer/-/puppeteer-4.0.0.tgz", @@ -4160,12 +4153,6 @@ "node": ">=6.9.0" } }, - "node_modules/@base2/pretty-print-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz", - "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==", - "dev": true - }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -4482,6 +4469,19 @@ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, "node_modules/@emotion/jest": { "version": "11.7.1", "resolved": "https://registry.npmjs.org/@emotion/jest/-/jest-11.7.1.tgz", @@ -4633,33 +4633,11 @@ } } }, - "node_modules/@emotion/styled/node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/styled/node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", - "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", - "dev": true, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@emotion/utils": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", @@ -5137,12 +5115,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@fal-works/esbuild-plugin-global-externals": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", - "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", - "dev": true - }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -5174,20 +5146,6 @@ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", "license": "MIT" }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, "node_modules/@floating-ui/utils": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", @@ -5244,18 +5202,18 @@ } }, "node_modules/@geometricpanda/storybook-addon-badges": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@geometricpanda/storybook-addon-badges/-/storybook-addon-badges-2.0.1.tgz", - "integrity": "sha512-dCEK/xJewuFe1d+ndF0hQIAJRnUsV9q5kuDmp7zvO7fTd7cDz0X9Bjz0lNRn6n4Z9bL9/iFHKzJESDHFfs4ihQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@geometricpanda/storybook-addon-badges/-/storybook-addon-badges-2.0.5.tgz", + "integrity": "sha512-FH56ly6ZhltjyKQWxUKORP67BxhL9FMJRByS5lqKZpeP8J2MMsMXG7eQmFXKcZGQORfVQye+1uYYWXweDOiFTQ==", "dev": true, "peerDependencies": { - "@storybook/blocks": "^7.0.0", - "@storybook/components": "^7.0.0", - "@storybook/core-events": "^7.0.0", - "@storybook/manager-api": "^7.0.0", - "@storybook/preview-api": "^7.0.0", - "@storybook/theming": "^7.0.0", - "@storybook/types": "^7.0.0", + "@storybook/blocks": "^8.3.0", + "@storybook/components": "^8.3.0", + "@storybook/core-events": "^8.3.0", + "@storybook/manager-api": "^8.3.0", + "@storybook/preview-api": "^8.3.0", + "@storybook/theming": "^8.3.0", + "@storybook/types": "^8.3.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" }, @@ -5309,6 +5267,264 @@ "node": ">=6.9.0" } }, + "node_modules/@inquirer/checkbox": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.3.tgz", + "integrity": "sha512-CEt9B4e8zFOGtc/LYeQx5m8nfqQeG/4oNNv0PUvXGG0mys+wR/WbJ3B4KfSQ4Fcr3AQfpiuFOi3fVvmPfvNbxw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.0.tgz", + "integrity": "sha512-osaBbIMEqVFjTX5exoqPXs6PilWQdjaLhGtMDXMXg/yxkHXNq43GlxGyTA35lK2HpzUgDN+Cjh/2AmqCN0QJpw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.1.tgz", + "integrity": "sha512-rmZVXy9iZvO3ZStEe/ayuuwIJ23LSF13aPMlLMTQARX6lGUBDHGV8UB5i9MRrfy0+mZwt5/9bdy8llszSD3NQA==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.0.tgz", + "integrity": "sha512-Z3LeGsD3WlItDqLxTPciZDbGtm0wrz7iJGS/uUxSiQxef33ZrBq7LhsXg30P7xrWz1kZX4iGzxxj5SKZmJ8W+w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.3.tgz", + "integrity": "sha512-MDszqW4HYBpVMmAoy/FA9laLrgo899UAga0itEjsYrBthKieDZNc0e16gdn7N3cQ0DSf/6zsTBZMuDYDQU4ktg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", + "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.0.tgz", + "integrity": "sha512-16B8A9hY741yGXzd8UJ9R8su/fuuyO2e+idd7oVLYjP23wKJ6ILRIIHcnXe8/6AoYgwRS2zp4PNsW/u/iZ24yg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.3.tgz", + "integrity": "sha512-HA/W4YV+5deKCehIutfGBzNxWH1nhvUC67O4fC9ufSijn72yrYnRmzvC61dwFvlXIG1fQaYWi+cqNE9PaB9n6Q==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.3.tgz", + "integrity": "sha512-3qWjk6hS0iabG9xx0U1plwQLDBc/HA/hWzLFFatADpR6XfE62LqPr9GpFXBkLU0KQUaIXZ996bNG+2yUvocH8w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.2.0.tgz", + "integrity": "sha512-ZXYZ5oGVrb+hCzcglPeVerJ5SFwennmDOPfXq1WyeZIrPGySLbl4W6GaSsBFvu3WII36AOK5yB8RMIEEkBjf8w==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.0.3", + "@inquirer/confirm": "^5.1.0", + "@inquirer/editor": "^4.2.0", + "@inquirer/expand": "^4.0.3", + "@inquirer/input": "^4.1.0", + "@inquirer/number": "^3.0.3", + "@inquirer/password": "^4.0.3", + "@inquirer/rawlist": "^4.0.3", + "@inquirer/search": "^3.0.3", + "@inquirer/select": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.3.tgz", + "integrity": "sha512-5MhinSzfmOiZlRoPezfbJdfVCZikZs38ja3IOoWe7H1dxL0l3Z2jAUgbBldeyhhOkELdGvPlBfQaNbeLslib1w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.3.tgz", + "integrity": "sha512-mQTCbdNolTGvGGVCJSI6afDwiSGTV+fMLPEIMDJgIV6L/s3+RYRpxt6t0DYnqMQmemnZ/Zq0vTIRwoHT1RgcTg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.3.tgz", + "integrity": "sha512-OZfKDtDE8+J54JYAFTUGZwvKNfC7W/gFCjDkcsO7HnTH/wljsZo9y/FJquOxMy++DY0+9l9o/MOZ8s5s1j5wmw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.1", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.1.tgz", + "integrity": "sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -5877,9 +6093,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -5896,12 +6112,6 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true }, - "node_modules/@juggle/resize-observer": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", - "dev": true - }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -6938,19 +7148,19 @@ } }, "node_modules/@mdx-js/react": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-2.3.0.tgz", - "integrity": "sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", + "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", "dev": true, "dependencies": { - "@types/mdx": "^2.0.0", - "@types/react": ">=16" + "@types/mdx": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" }, "peerDependencies": { + "@types/react": ">=16", "react": ">=16" } }, @@ -6966,28 +7176,6 @@ "@tybys/wasm-util": "^0.9.0" } }, - "node_modules/@ndelangen/get-tarball": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@ndelangen/get-tarball/-/get-tarball-3.0.9.tgz", - "integrity": "sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==", - "dev": true, - "dependencies": { - "gunzip-maybe": "^1.4.2", - "pump": "^3.0.0", - "tar-fs": "^2.1.1" - } - }, - "node_modules/@ndelangen/get-tarball/node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -8815,15 +9003,6 @@ "streamx": "^2.15.0" } }, - "node_modules/@radix-ui/number": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.1.tgz", - "integrity": "sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -8833,57 +9012,6 @@ "@babel/runtime": "^7.13.10" } }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", - "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", @@ -8920,53 +9048,6 @@ } } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", - "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", - "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", @@ -8985,33 +9066,6 @@ } } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", - "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-id": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", @@ -9031,64 +9085,6 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", - "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", - "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-primitive": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", @@ -9113,106 +9109,6 @@ } } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", - "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-select": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-1.2.2.tgz", - "integrity": "sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.0.3.tgz", - "integrity": "sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -9232,92 +9128,6 @@ } } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", - "integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz", - "integrity": "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-toggle": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.0.4.tgz", - "integrity": "sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-separator": "1.0.3", - "@radix-ui/react-toggle-group": "1.0.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", @@ -9392,95 +9202,6 @@ } } }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz", - "integrity": "sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", - "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", - "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, "node_modules/@react-native-clipboard/clipboard": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.11.2.tgz", @@ -11610,6 +11331,12 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@remote-ui/rpc": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@remote-ui/rpc/-/rpc-1.4.5.tgz", + "integrity": "sha512-Cr+06niG/vmE4A9YsmaKngRuuVSWKMY42NMwtZfy+gctRWGu6Wj9BWuMJg5CEp+JTkRBPToqT5rqnrg1G/Wvow==", + "license": "MIT" + }, "node_modules/@samverschueren/stream-to-observable": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", @@ -11709,6 +11436,34 @@ "node": ">=8" } }, + "node_modules/@shopify/web-worker": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@shopify/web-worker/-/web-worker-6.4.0.tgz", + "integrity": "sha512-RvY1mgRyAqawFiYBvsBkek2pVK4GVpV9mmhWFCZXwx01usxXd2HMhKNTFeRYhSp29uoUcfBlKZAwCwQzt826tg==", + "license": "MIT", + "dependencies": { + "@remote-ui/rpc": "^1.2.5" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": "^5.38.0", + "webpack-virtual-modules": "^0.4.3 || ^0.5.0 || ^0.6.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "webpack": { + "optional": true + }, + "webpack-virtual-modules": { + "optional": true + } + } + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -11874,26 +11629,28 @@ } }, "node_modules/@storybook/addon-a11y": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-7.6.15.tgz", - "integrity": "sha512-8PxRMBJUSxNoceo2IYXFyZp3VU+/ONK/DsD0dj/fVrv7izFrS8aw2GWSsSMK8xAbEUpANXWMKGaSyvrRFVgsVQ==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.4.7.tgz", + "integrity": "sha512-GpUvXp6n25U1ZSv+hmDC+05BEqxWdlWjQTb/GaboRXZQeMBlze6zckpVb66spjmmtQAIISo0eZxX1+mGcVR7lA==", "dev": true, "dependencies": { - "@storybook/addon-highlight": "7.6.15", + "@storybook/addon-highlight": "8.4.7", "axe-core": "^4.2.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-actions": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.15.tgz", - "integrity": "sha512-2Jfvbahe/tmq1iNnNxmcP0JnX0rqCuijjXXai9yMDV3koIMawn6t88MPVrdcso5ch/fxE45522nZqA3SZJbM4g==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.4.7.tgz", + "integrity": "sha512-mjtD5JxcPuW74T6h7nqMxWTvDneFtokg88p6kQ5OnC1M259iAXb//yiSZgu/quunMHPCXSiqn4FNOSgASTSbsA==", "dev": true, "dependencies": { - "@storybook/core-events": "7.6.15", "@storybook/global": "^5.0.0", "@types/uuid": "^9.0.1", "dequal": "^2.0.2", @@ -11903,6 +11660,9 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-actions/node_modules/@types/uuid": { @@ -11912,44 +11672,35 @@ "dev": true }, "node_modules/@storybook/addon-controls": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-7.6.15.tgz", - "integrity": "sha512-HXcG/Lr4ri7WUFz14Y5lEBTA1XmKy0E/DepW88XVy6YNsTpERVWEBcvjKoLAU1smKrfhVto96hK2AVFL3A8EBQ==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.4.7.tgz", + "integrity": "sha512-377uo5IsJgXLnQLJixa47+11V+7Wn9KcDEw+96aGCBCfLbWNH8S08tJHHnSu+jXg9zoqCAC23MetntVp6LetHA==", "dev": true, "dependencies": { - "@storybook/blocks": "7.6.15", - "lodash": "^4.17.21", + "@storybook/global": "^5.0.0", + "dequal": "^2.0.2", "ts-dedent": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-docs": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-7.6.15.tgz", - "integrity": "sha512-UPODqO+mrYaKyTSAtfRslxOFgSP/v/5vfDx896pbNTC4Sf8xLytoudw4I14hzkHmRdXiOnd21FqXJfmF/Onsvw==", - "dev": true, - "dependencies": { - "@jest/transform": "^29.3.1", - "@mdx-js/react": "^2.1.5", - "@storybook/blocks": "7.6.15", - "@storybook/client-logger": "7.6.15", - "@storybook/components": "7.6.15", - "@storybook/csf-plugin": "7.6.15", - "@storybook/csf-tools": "7.6.15", - "@storybook/global": "^5.0.0", - "@storybook/mdx2-csf": "^1.0.0", - "@storybook/node-logger": "7.6.15", - "@storybook/postinstall": "7.6.15", - "@storybook/preview-api": "7.6.15", - "@storybook/react-dom-shim": "7.6.15", - "@storybook/theming": "7.6.15", - "@storybook/types": "7.6.15", - "fs-extra": "^11.1.0", - "remark-external-links": "^8.0.0", - "remark-slug": "^6.0.0", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.4.7.tgz", + "integrity": "sha512-NwWaiTDT5puCBSUOVuf6ME7Zsbwz7Y79WF5tMZBx/sLQ60vpmJVQsap6NSjvK1Ravhc21EsIXqemAcBjAWu80w==", + "dev": true, + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/blocks": "8.4.7", + "@storybook/csf-plugin": "8.4.7", + "@storybook/react-dom-shim": "8.4.7", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "ts-dedent": "^2.0.0" }, "funding": { @@ -11957,49 +11708,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/addon-docs/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/addon-docs/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/@storybook/addon-docs/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" + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-highlight": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-7.6.15.tgz", - "integrity": "sha512-ptidWZJJcEM83YsxCjf+m1q8Rr9sN8piJ4PJlM2vyc4MLZY4q6htb1JJFeq3ov1Iz6SY9KjKc/zOkWo4L54nxw==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.4.7.tgz", + "integrity": "sha512-whQIDBd3PfVwcUCrRXvCUHWClXe9mQ7XkTPCdPo4B/tZ6Z9c6zD8JUHT76ddyHivixFLowMnA8PxMU6kCMAiNw==", "dev": true, "dependencies": { "@storybook/global": "^5.0.0" @@ -12007,342 +11722,136 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-toolbars": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-7.6.15.tgz", - "integrity": "sha512-QougKS2eABB5Jd332i9tBpKgh2lN4aaqXkvmVC5egT5dOuJ9IeuZbGwiALef/uf1f3IuyUP41So9l2dI4u19aw==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.4.7.tgz", + "integrity": "sha512-OSfdv5UZs+NdGB+nZmbafGUWimiweJ/56gShlw8Neo/4jOJl1R3rnRqqY7MYx8E4GwoX+i3GF5C3iWFNQqlDcw==", "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, "node_modules/@storybook/addon-viewport": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-7.6.15.tgz", - "integrity": "sha512-0esg0+onJftU2prD3n/sbxBTrTOIGQnZhbrKPP+/S26dVHuYaR/65XdwpRgXNY5PHK2yjU78HxiJP+Kyu75ntw==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.4.7.tgz", + "integrity": "sha512-hvczh/jjuXXcOogih09a663sRDDSATXwbE866al1DXgbDFraYD/LxX/QDb38W9hdjU9+Qhx8VFIcNWoMQns5HQ==", "dev": true, "dependencies": { "memoizerific": "^1.11.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/blocks": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-7.6.15.tgz", - "integrity": "sha512-ODP7AVh2iIGblI5WKGokWSHbp9YQHc+Uce7JCGcnDbNavoy64Z6R6G+wXzF5jfl7xQlbhQ8yQCuSSL4GNdYTeA==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.15", - "@storybook/client-logger": "7.6.15", - "@storybook/components": "7.6.15", - "@storybook/core-events": "7.6.15", - "@storybook/csf": "^0.1.2", - "@storybook/docs-tools": "7.6.15", - "@storybook/global": "^5.0.0", - "@storybook/manager-api": "7.6.15", - "@storybook/preview-api": "7.6.15", - "@storybook/theming": "7.6.15", - "@storybook/types": "7.6.15", - "@types/lodash": "^4.14.167", - "color-convert": "^2.0.1", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "markdown-to-jsx": "^7.1.8", - "memoizerific": "^1.11.3", - "polished": "^4.2.2", - "react-colorful": "^5.1.2", - "telejson": "^7.2.0", - "tocbot": "^4.20.1", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "storybook": "^8.4.7" } }, - "node_modules/@storybook/blocks/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@storybook/addon-webpack5-compiler-babel": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@storybook/addon-webpack5-compiler-babel/-/addon-webpack5-compiler-babel-3.0.3.tgz", + "integrity": "sha512-rVQTTw+oxJltbVKaejIWSHwVKOBJs3au21f/pYXhV0aiNgNhxEa3vr79t/j0j8ox8uJtzM8XYOb7FlkvGfHlwQ==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "@babel/core": "^7.23.7", + "babel-loader": "^9.1.3" }, "engines": { - "node": ">=7.0.0" + "node": ">=18" } }, - "node_modules/@storybook/blocks/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@storybook/builder-manager": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/builder-manager/-/builder-manager-7.6.15.tgz", - "integrity": "sha512-vfpfCywiasyP7vtbgLJhjssBEwUjZhBsRsubDAzumgOochPiKKPNwsSc5NU/4ZIGaC5zRO26kUaUqFIbJdTEUQ==", + "node_modules/@storybook/blocks": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.4.7.tgz", + "integrity": "sha512-+QH7+JwXXXIyP3fRCxz/7E2VZepAanXJM7G8nbR3wWsqWgrRp4Wra6MvybxAYCxU7aNfJX5c+RW84SNikFpcIA==", "dev": true, "dependencies": { - "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@storybook/core-common": "7.6.15", - "@storybook/manager": "7.6.15", - "@storybook/node-logger": "7.6.15", - "@types/ejs": "^3.1.1", - "@types/find-cache-dir": "^3.2.1", - "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.10", - "browser-assert": "^1.2.1", - "ejs": "^3.1.8", - "esbuild": "^0.18.0", - "esbuild-plugin-alias": "^0.2.1", - "express": "^4.17.3", - "find-cache-dir": "^3.0.0", - "fs-extra": "^11.1.0", - "process": "^0.11.10", - "util": "^0.12.4" + "@storybook/csf": "^0.1.11", + "@storybook/icons": "^1.2.12", + "ts-dedent": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/builder-manager/node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/@storybook/builder-manager/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/builder-manager/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/builder-manager/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/@storybook/builder-manager/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/builder-manager/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/builder-manager/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/builder-manager/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/builder-manager/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@storybook/builder-manager/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/builder-manager/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/builder-manager/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@storybook/builder-manager/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" + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, - "node_modules/@storybook/builder-manager/node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "node_modules/@storybook/builder-webpack5": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.4.7.tgz", + "integrity": "sha512-O8LpsQ+4g2x5kh7rI9+jEUdX8k1a5egBQU1lbudmHchqsV0IKiVqBD9LL5Gj3wpit4vB8coSW4ZWTFBw8FQb4Q==", "dev": true, "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/@storybook/builder-webpack5": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-7.6.15.tgz", - "integrity": "sha512-HF+TSK/eU2ld8uQ8VWgcAIzOQ2hjnEkzup363vGZkYUfsHsVbjMpZgf+foDjI4LZNfQ/RjcVEZxqJqIbpM0Sjg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.2", - "@storybook/channels": "7.6.15", - "@storybook/client-logger": "7.6.15", - "@storybook/core-common": "7.6.15", - "@storybook/core-events": "7.6.15", - "@storybook/core-webpack": "7.6.15", - "@storybook/node-logger": "7.6.15", - "@storybook/preview": "7.6.15", - "@storybook/preview-api": "7.6.15", - "@swc/core": "^1.3.82", - "@types/node": "^18.0.0", + "@storybook/core-webpack": "8.4.7", + "@types/node": "^22.0.0", "@types/semver": "^7.3.4", - "babel-loader": "^9.0.0", "browser-assert": "^1.2.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", "cjs-module-lexer": "^1.2.3", "constants-browserify": "^1.0.0", "css-loader": "^6.7.1", - "es-module-lexer": "^1.4.1", - "express": "^4.17.3", + "es-module-lexer": "^1.5.0", "fork-ts-checker-webpack-plugin": "^8.0.0", - "fs-extra": "^11.1.0", "html-webpack-plugin": "^5.5.0", "magic-string": "^0.30.5", "path-browserify": "^1.0.1", "process": "^0.11.10", "semver": "^7.3.7", "style-loader": "^3.3.1", - "swc-loader": "^0.2.3", "terser-webpack-plugin": "^5.3.1", "ts-dedent": "^2.0.0", "url": "^0.11.0", "util": "^0.12.4", "util-deprecate": "^1.0.2", "webpack": "5", - "webpack-dev-middleware": "^6.1.1", + "webpack-dev-middleware": "^6.1.2", "webpack-hot-middleware": "^2.25.1", - "webpack-virtual-modules": "^0.5.0" + "webpack-virtual-modules": "^0.6.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, + "peerDependencies": { + "storybook": "^8.4.7" + }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, + "node_modules/@storybook/builder-webpack5/node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@storybook/builder-webpack5/node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -12378,32 +11887,6 @@ } } }, - "node_modules/@storybook/builder-webpack5/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/builder-webpack5/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/@storybook/builder-webpack5/node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -12432,14 +11915,12 @@ "webpack": "^5.0.0" } }, - "node_modules/@storybook/builder-webpack5/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/@storybook/builder-webpack5/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "engines": { - "node": ">= 10.0.0" - } + "license": "MIT" }, "node_modules/@storybook/builder-webpack5/node_modules/util": { "version": "0.12.5", @@ -12454,1656 +11935,435 @@ "which-typed-array": "^1.1.2" } }, - "node_modules/@storybook/channels": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.15.tgz", - "integrity": "sha512-UPDYRzGkygYFa8QUpEiumWrvZm4u4RKVzgiBt9C4RmHORqkkZzL9LXhaZJp2SmIz1ND5gx6KR5ze8ZnAdwxxoQ==", + "node_modules/@storybook/components": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.7.tgz", + "integrity": "sha512-uyJIcoyeMWKAvjrG9tJBUCKxr2WZk+PomgrgrUwejkIfXMO76i6jw9BwLa0NZjYdlthDv30r9FfbYZyeNPmF0g==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" + } + }, + "node_modules/@storybook/core": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.4.7.tgz", + "integrity": "sha512-7Z8Z0A+1YnhrrSXoKKwFFI4gnsLbWzr8fnDCU6+6HlDukFYh8GHRcZ9zKfqmy6U3hw2h8H5DrHsxWfyaYUUOoA==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.15", - "@storybook/core-events": "7.6.15", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" + "@storybook/csf": "^0.1.11", + "better-opn": "^3.0.2", + "browser-assert": "^1.2.1", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0", + "esbuild-register": "^3.5.0", + "jsdoc-type-pratt-parser": "^4.0.0", + "process": "^0.11.10", + "recast": "^0.23.5", + "semver": "^7.6.2", + "util": "^0.12.5", + "ws": "^8.2.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } } }, - "node_modules/@storybook/cli": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.15.tgz", - "integrity": "sha512-2QRqCyVGDSkraHxX2JPYkkFccbu5Uo+JYFaFJo4vmMXzDurjWON+Ga2B8FCTd4A8P4C02Ca/79jgQoyBB3xoew==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.2", - "@babel/preset-env": "^7.23.2", - "@babel/types": "^7.23.0", - "@ndelangen/get-tarball": "^3.0.7", - "@storybook/codemod": "7.6.15", - "@storybook/core-common": "7.6.15", - "@storybook/core-events": "7.6.15", - "@storybook/core-server": "7.6.15", - "@storybook/csf-tools": "7.6.15", - "@storybook/node-logger": "7.6.15", - "@storybook/telemetry": "7.6.15", - "@storybook/types": "7.6.15", - "@types/semver": "^7.3.4", - "@yarnpkg/fslib": "2.10.3", - "@yarnpkg/libzip": "2.3.0", - "chalk": "^4.1.0", - "commander": "^6.2.1", - "cross-spawn": "^7.0.3", - "detect-indent": "^6.1.0", - "envinfo": "^7.7.3", - "execa": "^5.0.0", - "express": "^4.17.3", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "get-npm-tarball-url": "^2.0.3", - "get-port": "^5.1.1", - "giget": "^1.0.0", - "globby": "^11.0.2", - "jscodeshift": "^0.15.1", - "leven": "^3.1.0", - "ora": "^5.4.1", - "prettier": "^2.8.0", - "prompts": "^2.4.0", - "puppeteer-core": "^2.1.1", - "read-pkg-up": "^7.0.1", - "semver": "^7.3.7", - "strip-json-comments": "^3.0.1", - "tempy": "^1.0.1", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, - "bin": { - "getstorybook": "bin/index.js", - "sb": "bin/index.js" + "node_modules/@storybook/core-webpack": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-8.4.7.tgz", + "integrity": "sha512-Tj+CjQLpFyBJxhhMms+vbPT3+gTRAiQlrhY3L1IEVwBa3wtRMS0qjozH26d1hK4G6mUIEdwu13L54HMU/w33Sg==", + "dev": true, + "dependencies": { + "@types/node": "^22.0.0", + "ts-dedent": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, - "node_modules/@storybook/cli/node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "node_modules/@storybook/core-webpack/node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, - "engines": { - "node": ">= 6.0.0" + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" } }, - "node_modules/@storybook/cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@storybook/core-webpack/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/core/node_modules/recast": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", + "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 4" } }, - "node_modules/@storybook/cli/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "node_modules/@storybook/core/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "restore-cursor": "^3.1.0" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/@storybook/cli/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "node_modules/@storybook/core/node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, - "engines": { - "node": ">= 6" + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" } }, - "node_modules/@storybook/cli/node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "node_modules/@storybook/core/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/@storybook/cli/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/@storybook/csf": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.11.tgz", + "integrity": "sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==", "dev": true, + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" + "type-fest": "^2.19.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.4.7.tgz", + "integrity": "sha512-Fgogplu4HImgC+AYDcdGm1rmL6OR1rVdNX1Be9C/NEXwOCpbbBwi0BxTf/2ZxHRk9fCeaPEcOdP5S8QHfltc1g==", + "dev": true, + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" }, + "peerDependencies": { + "storybook": "^8.4.7" + } + }, + "node_modules/@storybook/csf/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/cli/node_modules/find-up": { + "node_modules/@storybook/global": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true + }, + "node_modules/@storybook/icons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.3.0.tgz", + "integrity": "sha512-Nz/UzeYQdUZUhacrPyfkiiysSjydyjgg/p0P9HxB4p/WaJUUjMAcaoaLgy3EXx61zZJ3iD36WPuDkZs5QYrA0A==", "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=10" + "node": ">=14.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" } }, - "node_modules/@storybook/cli/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/@storybook/instrumenter": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.4.7.tgz", + "integrity": "sha512-k6NSD3jaRCCHAFtqXZ7tw8jAzD/yTEWXGya+REgZqq5RCkmJ+9S4Ytp/6OhQMPtPFX23gAuJJzTQVLcCr+gjRg==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@storybook/global": "^5.0.0", + "@vitest/utils": "^2.1.1" }, - "engines": { - "node": ">=14.14" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, - "node_modules/@storybook/cli/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/@storybook/instrumenter/node_modules/@vitest/pretty-format": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", + "integrity": "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==", "dev": true, - "engines": { - "node": ">=10" + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@storybook/cli/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/@storybook/instrumenter/node_modules/@vitest/utils": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.8.tgz", + "integrity": "sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/cli/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/@storybook/cli/node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, - "dependencies": { - "agent-base": "5", - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@storybook/cli/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@storybook/cli/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/cli/node_modules/jscodeshift": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", - "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.23.0", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/preset-flow": "^7.22.15", - "@babel/preset-typescript": "^7.23.0", - "@babel/register": "^7.22.15", - "babel-core": "^7.0.0-bridge.0", - "chalk": "^4.1.2", - "flow-parser": "0.*", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.4", - "neo-async": "^2.5.0", - "node-dir": "^0.1.17", - "recast": "^0.23.3", - "temp": "^0.8.4", - "write-file-atomic": "^2.3.0" - }, - "bin": { - "jscodeshift": "bin/jscodeshift.js" - }, - "peerDependencies": { - "@babel/preset-env": "^7.1.6" - }, - "peerDependenciesMeta": { - "@babel/preset-env": { - "optional": true - } - } - }, - "node_modules/@storybook/cli/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/@storybook/cli/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/cli/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@storybook/cli/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@storybook/cli/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/cli/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/cli/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/cli/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/cli/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/cli/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/@storybook/cli/node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/@storybook/cli/node_modules/puppeteer-core": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-2.1.1.tgz", - "integrity": "sha512-n13AWriBMPYxnpbb6bnaY5YoY6rGj8vPLrz6CZF3o0qJNEwlcfJVxBzYZ0NJsQ21UbdJoijPCDrM++SUVEz7+w==", - "dev": true, - "dependencies": { - "@types/mime-types": "^2.1.0", - "debug": "^4.1.0", - "extract-zip": "^1.6.6", - "https-proxy-agent": "^4.0.0", - "mime": "^2.0.3", - "mime-types": "^2.1.25", - "progress": "^2.0.1", - "proxy-from-env": "^1.0.0", - "rimraf": "^2.6.1", - "ws": "^6.1.0" - }, - "engines": { - "node": ">=8.16.0" - } - }, - "node_modules/@storybook/cli/node_modules/recast": { - "version": "0.23.9", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", - "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@storybook/cli/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/cli/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/@storybook/cli/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/cli/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/@storybook/cli/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/@storybook/cli/node_modules/ws": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", - "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/@storybook/client-logger": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.15.tgz", - "integrity": "sha512-n+K8IqnombqiQNnywVovS+lK61tvv/XSfgPt0cgvoF/hJZB0VDOMRjWsV+v9qQpj1TQEl1lLWeJwZMthTWupJA==", - "dev": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.15.tgz", - "integrity": "sha512-NiEbTLCdacj6TMxC7G49IImXeMzkG8wpPr8Ayxm9HeG6q5UkiF5/DiZdqbJm2zaosOsOKWwvXg1t6Pq6Nivytg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.2", - "@babel/preset-env": "^7.23.2", - "@babel/types": "^7.23.0", - "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.15", - "@storybook/node-logger": "7.6.15", - "@storybook/types": "7.6.15", - "@types/cross-spawn": "^6.0.2", - "cross-spawn": "^7.0.3", - "globby": "^11.0.2", - "jscodeshift": "^0.15.1", - "lodash": "^4.17.21", - "prettier": "^2.8.0", - "recast": "^0.23.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/codemod/node_modules/chalk": { - "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": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@storybook/codemod/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/@storybook/codemod/node_modules/jscodeshift": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", - "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.23.0", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", - "@babel/plugin-transform-optional-chaining": "^7.23.0", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/preset-flow": "^7.22.15", - "@babel/preset-typescript": "^7.23.0", - "@babel/register": "^7.22.15", - "babel-core": "^7.0.0-bridge.0", - "chalk": "^4.1.2", - "flow-parser": "0.*", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.4", - "neo-async": "^2.5.0", - "node-dir": "^0.1.17", - "recast": "^0.23.3", - "temp": "^0.8.4", - "write-file-atomic": "^2.3.0" - }, - "bin": { - "jscodeshift": "bin/jscodeshift.js" - }, - "peerDependencies": { - "@babel/preset-env": "^7.1.6" - }, - "peerDependenciesMeta": { - "@babel/preset-env": { - "optional": true - } - } - }, - "node_modules/@storybook/codemod/node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/@storybook/codemod/node_modules/recast": { - "version": "0.23.9", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", - "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/@storybook/codemod/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/@storybook/components": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.6.15.tgz", - "integrity": "sha512-xD+maP7+C9HeZXi2vJ+uK9hXN4S4spP4uDj9pyZ9yViKb+ztEO6WpovUMT8WRQ0mMegWyLXkx3zqu43hZvXM1g==", - "dev": true, - "dependencies": { - "@radix-ui/react-select": "^1.2.2", - "@radix-ui/react-toolbar": "^1.0.4", - "@storybook/client-logger": "7.6.15", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/theming": "7.6.15", - "@storybook/types": "7.6.15", - "memoizerific": "^1.11.3", - "use-resize-observer": "^9.1.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/core-client": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/core-client/-/core-client-7.6.15.tgz", - "integrity": "sha512-jwWol+zo+ItKBzPm9i80bEL6seHMsV0wKSaViVMQ4TqHtEbNeFE8sFEc2NTr18VNBnQOdlQPnEWmdboXBUrGcA==", - "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.15", - "@storybook/preview-api": "7.6.15" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-common": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.15.tgz", - "integrity": "sha512-VGmcLJ5U1r1s8/YnLbKcyB4GnNL+/sZIPqwlcSKzDXO76HoVFv1kywf7PbASote7P3gdhLSxBdg95LH2bdIbmw==", - "dev": true, - "dependencies": { - "@storybook/core-events": "7.6.15", - "@storybook/node-logger": "7.6.15", - "@storybook/types": "7.6.15", - "@types/find-cache-dir": "^3.2.1", - "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.4", - "@types/pretty-hrtime": "^1.0.0", - "chalk": "^4.1.0", - "esbuild": "^0.18.0", - "esbuild-register": "^3.5.0", - "file-system-cache": "2.3.0", - "find-cache-dir": "^3.0.0", - "find-up": "^5.0.0", - "fs-extra": "^11.1.0", - "glob": "^10.0.0", - "handlebars": "^4.7.7", - "lazy-universal-dotenv": "^4.0.0", - "node-fetch": "^2.0.0", - "picomatch": "^2.3.0", - "pkg-dir": "^5.0.0", - "pretty-hrtime": "^1.0.3", - "resolve-from": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-common/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/find-cache-dir/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/core-common/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/core-common/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/@storybook/core-common/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@storybook/core-common/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/@storybook/core-common/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-common/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@storybook/core-common/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/pkg-dir": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", - "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", - "dev": true, - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/core-common/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/core-common/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@storybook/core-common/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/@storybook/core-events": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.15.tgz", - "integrity": "sha512-i4YnjGecbpGyrFe0340sPhQ9QjZZEBqvMy6kF4XWt6DYLHxZmsTj1HEdvxVl4Ej7V49Vw0Dm8MepJ1d4Y8MKrQ==", - "dev": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.15.tgz", - "integrity": "sha512-iIlxEAkrmKTSA3iGNqt/4QG7hf5suxBGYIB3DZAOfBo8EdZogMYaEmuCm5dbuaJr0mcVwlqwdhQiWb1VsR/NhA==", - "dev": true, - "dependencies": { - "@aw-web-design/x-default-browser": "1.4.126", - "@discoveryjs/json-ext": "^0.5.3", - "@storybook/builder-manager": "7.6.15", - "@storybook/channels": "7.6.15", - "@storybook/core-common": "7.6.15", - "@storybook/core-events": "7.6.15", - "@storybook/csf": "^0.1.2", - "@storybook/csf-tools": "7.6.15", - "@storybook/docs-mdx": "^0.1.0", - "@storybook/global": "^5.0.0", - "@storybook/manager": "7.6.15", - "@storybook/node-logger": "7.6.15", - "@storybook/preview-api": "7.6.15", - "@storybook/telemetry": "7.6.15", - "@storybook/types": "7.6.15", - "@types/detect-port": "^1.3.0", - "@types/node": "^18.0.0", - "@types/pretty-hrtime": "^1.0.0", - "@types/semver": "^7.3.4", - "better-opn": "^3.0.2", - "chalk": "^4.1.0", - "cli-table3": "^0.6.1", - "compression": "^1.7.4", - "detect-port": "^1.3.0", - "express": "^4.17.3", - "fs-extra": "^11.1.0", - "globby": "^11.0.2", - "ip": "^2.0.0", - "lodash": "^4.17.21", - "open": "^8.4.0", - "pretty-hrtime": "^1.0.3", - "prompts": "^2.4.0", - "read-pkg-up": "^7.0.1", - "semver": "^7.3.7", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1", - "ts-dedent": "^2.0.0", - "util": "^0.12.4", - "util-deprecate": "^1.0.2", - "watchpack": "^2.2.0", - "ws": "^8.2.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/core-server/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/core-server/node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "dev": true - }, - "node_modules/@storybook/core-server/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" + "@vitest/pretty-format": "2.1.8", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@storybook/core-server/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/@storybook/manager-api": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.4.7.tgz", + "integrity": "sha512-ELqemTviCxAsZ5tqUz39sDmQkvhVAvAgiplYy9Uf15kO0SP2+HKsCMzlrm2ue2FfkUNyqbDayCPPCB0Cdn/mpQ==", "dev": true, - "dependencies": { - "universalify": "^2.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, - "node_modules/@storybook/core-server/node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "node_modules/@storybook/preset-react-webpack": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-8.4.7.tgz", + "integrity": "sha512-geTSBKyrBagVihil5MF7LkVFynbfHhCinvnbCZZqXW7M1vgcxvatunUENB+iV8eWg/0EJ+8O7scZL+BAxQ/2qg==", "dev": true, "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "@storybook/core-webpack": "8.4.7", + "@storybook/react": "8.4.7", + "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", + "@types/node": "^22.0.0", + "@types/semver": "^7.3.4", + "find-up": "^5.0.0", + "magic-string": "^0.30.5", + "react-docgen": "^7.0.0", + "resolve": "^1.22.8", + "semver": "^7.3.7", + "tsconfig-paths": "^4.2.0", + "webpack": "5" }, "engines": { - "node": ">=12" + "node": ">=18.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@storybook/core-server/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/@storybook/core-server/node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/@storybook/core-server/node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@storybook/core-server/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" + "type": "opencollective", + "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7" }, "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { + "typescript": { "optional": true } } }, - "node_modules/@storybook/core-webpack": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-7.6.15.tgz", - "integrity": "sha512-6Qk/kc7OKcy4jNowQFz6TFLWM2NYeLoJ73dIbFnN2o8DYS5WwmQLZhZ+MRvr92M+w1nlnc268kaqooYmAj8Mnw==", - "dev": true, - "dependencies": { - "@storybook/core-common": "7.6.15", - "@storybook/node-logger": "7.6.15", - "@storybook/types": "7.6.15", - "@types/node": "^18.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/csf": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.11.tgz", - "integrity": "sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==", + "node_modules/@storybook/preset-react-webpack/node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^2.19.0" - } - }, - "node_modules/@storybook/csf-plugin": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-7.6.15.tgz", - "integrity": "sha512-5Pm2B8XKNdG3fHyItWKbWnXHSRDFSvetlML+sMWGWYIjwOsnvPqt+gAvLksWhv/uJgDujGxNcPEh+/Y5C8ZAjQ==", - "dev": true, - "dependencies": { - "@storybook/csf-tools": "7.6.15", - "unplugin": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/csf-tools": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-7.6.15.tgz", - "integrity": "sha512-8iKgg2cmbFTpVhRRJOqouhPcEh0c8ywabG4S8ICZvnJooSXUI9mD9p3tYCS7MYuSiHj0epa1Kkn9DtXJRo9o6g==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.23.0", - "@babel/parser": "^7.23.0", - "@babel/traverse": "^7.23.2", - "@babel/types": "^7.23.0", - "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.15", - "fs-extra": "^11.1.0", - "recast": "^0.23.1", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "undici-types": "~6.20.0" } }, - "node_modules/@storybook/csf-tools/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/@storybook/preset-react-webpack/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=14.14" - } - }, - "node_modules/@storybook/csf-tools/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" + "node": ">=10" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/csf-tools/node_modules/recast": { - "version": "0.23.9", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", - "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", + "node_modules/@storybook/preset-react-webpack/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "MIT", "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" + "p-locate": "^5.0.0" }, "engines": { - "node": ">= 4" - } - }, - "node_modules/@storybook/csf-tools/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/@storybook/csf/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/docs-mdx": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@storybook/docs-mdx/-/docs-mdx-0.1.0.tgz", - "integrity": "sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==", - "dev": true - }, - "node_modules/@storybook/docs-tools": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/docs-tools/-/docs-tools-7.6.15.tgz", - "integrity": "sha512-npZEaI9Wpn9uJcRXFElqyiRw8bSxt95mLywPiEEGMT2kE5FfXM8d5Uj5O64kzoXdRI9IhRPEEZZidOtA/UInfQ==", - "dev": true, - "dependencies": { - "@storybook/core-common": "7.6.15", - "@storybook/preview-api": "7.6.15", - "@storybook/types": "7.6.15", - "@types/doctrine": "^0.0.3", - "assert": "^2.1.0", - "doctrine": "^3.0.0", - "lodash": "^4.17.21" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/docs-tools/node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, - "node_modules/@storybook/docs-tools/node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/@storybook/preset-react-webpack/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "dependencies": { - "esutils": "^2.0.2" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@storybook/docs-tools/node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/@storybook/global": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", - "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", - "dev": true - }, - "node_modules/@storybook/manager": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/manager/-/manager-7.6.15.tgz", - "integrity": "sha512-GGV2ElV5AOIApy/FSDzoSlLUbyd2VhQVD3TdNGRxNauYRjEO8ulXHw2tNbT6ludtpYpDTAILzI6zT/iag8hmPQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/manager-api": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.6.15.tgz", - "integrity": "sha512-cPBsXcnJiaO3QyaEum2JgdihYea3cI03FeV35JdrBYLIelT4oqbYFnzjznsFg9+Ia9iAbz7aOBNyyRsWnC/UKw==", - "dev": true, - "dependencies": { - "@storybook/channels": "7.6.15", - "@storybook/client-logger": "7.6.15", - "@storybook/core-events": "7.6.15", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/router": "7.6.15", - "@storybook/theming": "7.6.15", - "@storybook/types": "7.6.15", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "store2": "^2.14.2", - "telejson": "^7.2.0", - "ts-dedent": "^2.0.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/mdx2-csf": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@storybook/mdx2-csf/-/mdx2-csf-1.1.0.tgz", - "integrity": "sha512-TXJJd5RAKakWx4BtpwvSNdgTDkKM6RkXU8GK34S/LhidQ5Pjz3wcnqb0TxEkfhK/ztbP8nKHqXFwLfa2CYkvQw==", - "dev": true - }, - "node_modules/@storybook/node-logger": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-7.6.15.tgz", - "integrity": "sha512-C+sCvRjR+5uVU3VTrfyv7/RlPBxesAjIucUAK0keGyIZ7sFQYCPdkm4m/C4s+TcubgAzVvuoUHlRrSppdA7WzQ==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, - "node_modules/@storybook/postinstall": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/postinstall/-/postinstall-7.6.15.tgz", - "integrity": "sha512-DXQQ4kjAbQ7BSd9M4lDI/12vEEciYMP8uYFDlrPFjwD9LezsxtRiORkazjNRRX4730faO5zZsnWhXxCVkxck0g==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@storybook/preset-react-webpack": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-7.6.15.tgz", - "integrity": "sha512-Oo3J7RKO/tFUVnRXs16tZGcX6n90gTpHdlT2Z1fZ+y8wEd9o+VvvKFEIIeMcRxf3hHa49R6Kbc4AQaE9FAuDlw==", + "node_modules/@storybook/preset-react-webpack/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "dependencies": { - "@babel/preset-flow": "^7.22.15", - "@babel/preset-react": "^7.22.15", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", - "@storybook/core-webpack": "7.6.15", - "@storybook/docs-tools": "7.6.15", - "@storybook/node-logger": "7.6.15", - "@storybook/react": "7.6.15", - "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", - "@types/node": "^18.0.0", - "@types/semver": "^7.3.4", - "babel-plugin-add-react-displayname": "^0.0.5", - "fs-extra": "^11.1.0", - "magic-string": "^0.30.5", - "react-docgen": "^7.0.0", - "react-refresh": "^0.14.0", - "semver": "^7.3.7", - "webpack": "5" - }, "engines": { - "node": ">=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "@babel/core": "^7.22.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "typescript": { - "optional": true - } + "node": ">=8" } }, - "node_modules/@storybook/preset-react-webpack/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/@storybook/preset-react-webpack/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, "engines": { - "node": ">=14.14" + "node": ">=4" } }, - "node_modules/@storybook/preset-react-webpack/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/@storybook/preset-react-webpack/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "dependencies": { - "universalify": "^2.0.0" + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@storybook/preset-react-webpack/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": ">=6" } }, - "node_modules/@storybook/preview": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/preview/-/preview-7.6.15.tgz", - "integrity": "sha512-q8d9v0+Bo/DHLV68OyV3Klep4knf2GAbrlHhLW1X4jlPccuEDUojIfqfK7m48ayeIxJzO48fcO0JdKM1XABx7g==", + "node_modules/@storybook/preset-react-webpack/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } + "license": "MIT" }, "node_modules/@storybook/preview-api": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-7.6.15.tgz", - "integrity": "sha512-2KN9vlizF6sFlYsJEGnFqcQaJXs4TTdawC1VazVdtaMSHANDxxDu8F1cP+u7lpPH3DkNZUmTGQDBYfYY9xR0eQ==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.7.tgz", + "integrity": "sha512-0QVQwHw+OyZGHAJEXo6Knx+6/4er7n2rTDE5RYJ9F2E2Lg42E19pfdLlq2Jhoods2Xrclo3wj6GWR//Ahi39Eg==", "dev": true, - "dependencies": { - "@storybook/channels": "7.6.15", - "@storybook/client-logger": "7.6.15", - "@storybook/core-events": "7.6.15", - "@storybook/csf": "^0.1.2", - "@storybook/global": "^5.0.0", - "@storybook/types": "7.6.15", - "@types/qs": "^6.9.5", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "qs": "^6.10.0", - "synchronous-promise": "^2.0.15", - "ts-dedent": "^2.0.0", - "util-deprecate": "^1.0.2" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@storybook/react": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-7.6.15.tgz", - "integrity": "sha512-oJMSh4iTGu6OqCmj0LhkuPyMkxGMTCoohN4HcDpXd96jCSyWotVebRsg9xm5ddB7f54e6DY4XDoGH0WnVoR23g==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-8.4.7.tgz", + "integrity": "sha512-nQ0/7i2DkaCb7dy0NaT95llRVNYWQiPIVuhNfjr1mVhEP7XD090p0g7eqUmsx8vfdHh2BzWEo6CoBFRd3+EXxw==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.15", - "@storybook/core-client": "7.6.15", - "@storybook/docs-tools": "7.6.15", + "@storybook/components": "8.4.7", "@storybook/global": "^5.0.0", - "@storybook/preview-api": "7.6.15", - "@storybook/react-dom-shim": "7.6.15", - "@storybook/types": "7.6.15", - "@types/escodegen": "^0.0.6", - "@types/estree": "^0.0.51", - "@types/node": "^18.0.0", - "acorn": "^7.4.1", - "acorn-jsx": "^5.3.1", - "acorn-walk": "^7.2.0", - "escodegen": "^2.1.0", - "html-tags": "^3.1.0", - "lodash": "^4.17.21", - "prop-types": "^15.7.2", - "react-element-to-jsx-string": "^15.0.0", - "ts-dedent": "^2.0.0", - "type-fest": "~2.19", - "util-deprecate": "^1.0.2" + "@storybook/manager-api": "8.4.7", + "@storybook/preview-api": "8.4.7", + "@storybook/react-dom-shim": "8.4.7", + "@storybook/theming": "8.4.7" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "typescript": "*" + "@storybook/test": "8.4.7", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7", + "typescript": ">= 4.2.x" }, "peerDependenciesMeta": { + "@storybook/test": { + "optional": true + }, "typescript": { "optional": true } @@ -14252,112 +12512,84 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-7.6.15.tgz", - "integrity": "sha512-2+X0HIxIyvjfSKVyGGjSJJLEFJ2ox7Rr8FjlMiRo5QfoOJhohZuWH7p4Lw7JMwm5PotnjrwlfsZI3cCilYJeYA==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.4.7.tgz", + "integrity": "sha512-6bkG2jvKTmWrmVzCgwpTxwIugd7Lu+2btsLAqhQSzDyIj2/uhMNp8xIMr/NBDtLgq3nomt9gefNa9xxLwk/OMg==", "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7" } }, "node_modules/@storybook/react-webpack5": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-7.6.15.tgz", - "integrity": "sha512-TyYYSDho+4cQRBCVMKu7XDTCrAsLWaeldCoZm910e4DTXZUV3NDG8hVJIXzweaCu1o7JtDOelxsA6iizR/22GQ==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-8.4.7.tgz", + "integrity": "sha512-T9GLqlsP4It4El7cC8rSkBPRWvORAsTDULeWlO36RST2TrYnmBOUytsi22mk7cAAAVhhD6rTrs1YdqWRMpfa1w==", "dev": true, "dependencies": { - "@storybook/builder-webpack5": "7.6.15", - "@storybook/preset-react-webpack": "7.6.15", - "@storybook/react": "7.6.15", - "@types/node": "^18.0.0" + "@storybook/builder-webpack5": "8.4.7", + "@storybook/preset-react-webpack": "8.4.7", + "@storybook/react": "8.4.7", + "@types/node": "^22.0.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "@babel/core": "^7.22.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "typescript": "*" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.4.7", + "typescript": ">= 4.2.x" }, "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, "typescript": { "optional": true } } }, - "node_modules/@storybook/react/node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true - }, - "node_modules/@storybook/react/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/@storybook/react/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "node_modules/@storybook/react-webpack5/node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" } }, - "node_modules/@storybook/router": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.6.15.tgz", - "integrity": "sha512-5yhXXoVZ1iKUgeZoO8PGqBclrLgoJisxIYVK/Y1iJMXZ2ZvwUiTswLALT6lu97tSrcoBVxmqSghg0+U0YEU4Fg==", + "node_modules/@storybook/react-webpack5/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, - "dependencies": { - "@storybook/client-logger": "7.6.15", - "memoizerific": "^1.11.3", - "qs": "^6.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } + "license": "MIT" }, "node_modules/@storybook/source-loader": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-7.6.15.tgz", - "integrity": "sha512-E7LqjfvEUs2dn8ZWc1OfqzXU3vyi2/yP7rPHPRFjDUIpz1QI4IUCUIFY+n3YWkbk8wlmf6dV/2QYzYZPp6RD0g==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/source-loader/-/source-loader-8.4.7.tgz", + "integrity": "sha512-DrsYGGfNbbqlMzkhbLoNyNqrPa4QIkZ6O7FJ8Z/8jWb0cerQH2N6JW6k12ZnXgs8dO2Z33+iSEDIV8odh0E0PA==", "dev": true, "dependencies": { - "@storybook/csf": "^0.1.2", - "@storybook/types": "7.6.15", + "@storybook/csf": "^0.1.11", + "es-toolkit": "^1.22.0", "estraverse": "^5.2.0", - "lodash": "^4.17.21", - "prettier": "^2.8.0" + "prettier": "^3.1.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, "node_modules/@storybook/source-loader/node_modules/estraverse": { @@ -14370,109 +12602,200 @@ } }, "node_modules/@storybook/source-loader/node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/@storybook/telemetry": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.15.tgz", - "integrity": "sha512-klhKXLUS3OXozGEtMbbhKZLDfm+m3nNk2jvGwD6kkBenzFUzb0P2m8awxU7h1pBcKZKH/27U9t3KVzNFzWoWPw==", + "node_modules/@storybook/test": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.4.7.tgz", + "integrity": "sha512-AhvJsu5zl3uG40itSQVuSy5WByp3UVhS6xAnme4FWRwgSxhvZjATJ3AZkkHWOYjnnk+P2/sbz/XuPli1FVCWoQ==", "dev": true, "dependencies": { - "@storybook/client-logger": "7.6.15", - "@storybook/core-common": "7.6.15", - "@storybook/csf-tools": "7.6.15", - "chalk": "^4.1.0", - "detect-package-manager": "^2.0.1", - "fetch-retry": "^5.0.2", - "fs-extra": "^11.1.0", - "read-pkg-up": "^7.0.1" + "@storybook/csf": "^0.1.11", + "@storybook/global": "^5.0.0", + "@storybook/instrumenter": "8.4.7", + "@testing-library/dom": "10.4.0", + "@testing-library/jest-dom": "6.5.0", + "@testing-library/user-event": "14.5.2", + "@vitest/expect": "2.0.5", + "@vitest/spy": "2.0.5" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" } }, - "node_modules/@storybook/telemetry/node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/@storybook/test/node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" }, "engines": { - "node": ">=14.14" + "node": ">=18" } }, - "node_modules/@storybook/telemetry/node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/@storybook/test/node_modules/@testing-library/jest-dom": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", "dev": true, "dependencies": { - "universalify": "^2.0.0" + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/@storybook/telemetry/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/@storybook/test/node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">= 10.0.0" + "node": ">=8" } }, - "node_modules/@storybook/theming": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.6.15.tgz", - "integrity": "sha512-9PpsHAbUf6o0w33/P3mnb7QheTmfGlTYCismj5HMM1O2/zY0kQK9XcG9W+Cyvu56D/lFC19fz9YHQY8W4AbfnQ==", + "node_modules/@storybook/test/node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@storybook/test/node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@storybook/test/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.6.15", - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3" + "dequal": "^2.0.3" + } + }, + "node_modules/@storybook/test/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/@storybook/test/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@storybook/test/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@storybook/test/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/@storybook/test/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/@storybook/theming": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.7.tgz", + "integrity": "sha512-99rgLEjf7iwfSEmdqlHkSG3AyLcK0sfExcr0jnc6rLiAkBhzuIsvcHjjUwkR210SOCgXqBPW0ZA6uhnuyppHLw==", + "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@storybook/types": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.15.tgz", - "integrity": "sha512-tLH0lK6SXECSfMpKin9bge+7XiHZII17n6jc9ZI1TfSBZJyq3M6VzWh2r1C2lC97FlkcKXjIwM3n8h1xNjnI+A==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/types/-/types-8.4.7.tgz", + "integrity": "sha512-zuf0uPFjODB9Ls9/lqXnb1YsDKFuaASLOpTzpRlz9amFtTepo1dB0nVF9ZWcseTgGs7UxA4+ZR2SZrduXw/ihw==", "dev": true, - "dependencies": { - "@storybook/channels": "7.6.15", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@stylistic/stylelint-plugin": { @@ -14864,216 +13187,6 @@ "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@swc/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.1.tgz", - "integrity": "sha512-3y+Y8js+e7BbM16iND+6Rcs3jdiL28q3iVtYsCviYSSpP2uUVKkp5sJnCY4pg8AaVvyN7CGQHO7gLEZQ5ByozQ==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@swc/counter": "^0.1.2", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.4.1", - "@swc/core-darwin-x64": "1.4.1", - "@swc/core-linux-arm-gnueabihf": "1.4.1", - "@swc/core-linux-arm64-gnu": "1.4.1", - "@swc/core-linux-arm64-musl": "1.4.1", - "@swc/core-linux-x64-gnu": "1.4.1", - "@swc/core-linux-x64-musl": "1.4.1", - "@swc/core-win32-arm64-msvc": "1.4.1", - "@swc/core-win32-ia32-msvc": "1.4.1", - "@swc/core-win32-x64-msvc": "1.4.1" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.1.tgz", - "integrity": "sha512-ePyfx0348UbR4DOAW24TedeJbafnzha8liXFGuQ4bdXtEVXhLfPngprrxKrAddCuv42F9aTxydlF6+adD3FBhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.1.tgz", - "integrity": "sha512-eLf4JSe6VkCMdDowjM8XNC5rO+BrgfbluEzAVtKR8L2HacNYukieumN7EzpYCi0uF1BYwu1ku6tLyG2r0VcGxA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.1.tgz", - "integrity": "sha512-K8VtTLWMw+rkN/jDC9o/Q9SMmzdiHwYo2CfgkwVT29NsGccwmNhCQx6XoYiPKyKGIFKt4tdQnJHKUFzxUqQVtQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.1.tgz", - "integrity": "sha512-0e8p4g0Bfkt8lkiWgcdiENH3RzkcqKtpRXIVNGOmVc0OBkvc2tpm2WTx/eoCnes2HpTT4CTtR3Zljj4knQ4Fvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.1.tgz", - "integrity": "sha512-b/vWGQo2n7lZVUnSQ7NBq3Qrj85GrAPPiRbpqaIGwOytiFSk8VULFihbEUwDe0rXgY4LDm8z8wkgADZcLnmdUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.1.tgz", - "integrity": "sha512-AFMQlvkKEdNi1Vk2GFTxxJzbICttBsOQaXa98kFTeWTnFFIyiIj2w7Sk8XRTEJ/AjF8ia8JPKb1zddBWr9+bEQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.1.tgz", - "integrity": "sha512-QX2MxIECX1gfvUVZY+jk528/oFkS9MAl76e3ZRvG2KC/aKlCQL0KSzcTSm13mOxkDKS30EaGRDRQWNukGpMeRg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.1.tgz", - "integrity": "sha512-OklkJYXXI/tntD2zaY8i3iZldpyDw5q+NAP3k9OlQ7wXXf37djRsHLV0NW4+ZNHBjE9xp2RsXJ0jlOJhfgGoFA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.1.tgz", - "integrity": "sha512-MBuc3/QfKX9FnLOU7iGN+6yHRTQaPQ9WskiC8s8JFiKQ+7I2p25tay2RplR9dIEEGgVAu6L7auv96LbNTh+FaA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.1.tgz", - "integrity": "sha512-lu4h4wFBb/bOK6N2MuZwg7TrEpwYXgpQf5R7ObNSXL65BwZ9BG8XRzD+dLJmALu8l5N08rP/TrpoKRoGT4WSxw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true - }, - "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true - }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -15113,9 +13226,9 @@ "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==" }, "node_modules/@testing-library/dom": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -15535,43 +13648,10 @@ "@types/node": "*" } }, - "node_modules/@types/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/detect-port": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.5.tgz", - "integrity": "sha512-Rf3/lB9WkDfIL9eEKaSYKc+1L/rNVYBjThk22JTqQw0YozXarX8YljFAz+HCoC6h4B4KwCMsBPZHaFezwT4BNA==", - "dev": true - }, "node_modules/@types/doctrine": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.3.tgz", - "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", - "dev": true - }, - "node_modules/@types/ejs": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", - "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", - "dev": true - }, - "node_modules/@types/emscripten": { - "version": "1.39.10", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", - "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==", - "dev": true - }, - "node_modules/@types/escodegen": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.6.tgz", - "integrity": "sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", "dev": true }, "node_modules/@types/eslint": { @@ -15631,12 +13711,6 @@ "integrity": "sha512-g39Vp8ZJ3D0gXhhkhDidVvdy4QajkF7/PV6HGn23FMaMqE/tLC1JNHUeQ7SshKLsBjucakZsXBLkWULbGLdL5g==", "dev": true }, - "node_modules/@types/find-cache-dir": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", - "integrity": "sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==", - "dev": true - }, "node_modules/@types/find-root": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/find-root/-/find-root-1.1.2.tgz", @@ -15790,9 +13864,9 @@ "license": "MIT" }, "node_modules/@types/mdx": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.6.tgz", - "integrity": "sha512-sVcwEG10aFU2KcM7cIA0M410UPv/DesOPyG8zMVk0QUDexHA3lYmGucpEpZ2dtWWhi2ip3CG+5g/iH0PwoW4Fw==", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", "dev": true }, "node_modules/@types/method-override": { @@ -15811,12 +13885,6 @@ "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", "dev": true }, - "node_modules/@types/mime-types": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", - "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", - "dev": true - }, "node_modules/@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -15850,22 +13918,12 @@ } }, "node_modules/@types/node": { - "version": "18.19.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.59.tgz", - "integrity": "sha512-vizm2EqwV/7Zay+A6J3tGl9Lhr7CjZe2HmWS988sefiEmsyP9CeXEleho6i4hJk/8UtZAo0bWN4QPZZr83RxvQ==", + "version": "20.17.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz", + "integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==", "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" + "undici-types": "~6.19.2" } }, "node_modules/@types/node-forge": { @@ -15917,12 +13975,6 @@ "integrity": "sha512-ReVR2rLTV1kvtlWFyuot+d1pkpG2Fw/XKE3PDAdj57rbM97ttSp9JZ2UsP+2EHTylra9cUf6JA7tGwW1INzUrA==", "dev": true }, - "node_modules/@types/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-nj39q0wAIdhwn7DGUyT9irmsKK1tV0bd5WFEhgpqNTMFZ8cE+jieuTphCW0tfdm47S2zVT5mr09B28b1chmQMA==", - "dev": true - }, "node_modules/@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", @@ -16779,6 +14831,60 @@ "react": ">= 16.8.0" } }, + "node_modules/@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "estree-walker": "^3.0.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@wdio/config": { "version": "8.16.20", "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.16.20.tgz", @@ -17166,23 +15272,6 @@ "node": "^16.13 || >=18" } }, - "node_modules/@wdio/repl/node_modules/@types/node": { - "version": "20.17.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", - "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@wdio/repl/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "node_modules/@wdio/types": { "version": "8.16.12", "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.16.12.tgz", @@ -17196,23 +15285,6 @@ "node": "^16.13 || >=18" } }, - "node_modules/@wdio/types/node_modules/@types/node": { - "version": "20.17.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", - "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@wdio/types/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "node_modules/@wdio/utils": { "version": "8.16.17", "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.16.17.tgz", @@ -18080,6 +16152,10 @@ "resolved": "packages/undo-manager", "link": true }, + "node_modules/@wordpress/upload-media": { + "resolved": "packages/upload-media", + "link": true + }, "node_modules/@wordpress/url": { "resolved": "packages/url", "link": true @@ -18123,59 +16199,6 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, - "node_modules/@yarnpkg/esbuild-plugin-pnp": { - "version": "3.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@yarnpkg/esbuild-plugin-pnp/-/esbuild-plugin-pnp-3.0.0-rc.15.tgz", - "integrity": "sha512-kYzDJO5CA9sy+on/s2aIW0411AklfCi8Ck/4QDivOqsMKpStZA2SsR+X27VTggGwpStWaLrjJcDcdDMowtG8MA==", - "dev": true, - "dependencies": { - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "esbuild": ">=0.10.0" - } - }, - "node_modules/@yarnpkg/fslib": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.10.3.tgz", - "integrity": "sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A==", - "dev": true, - "dependencies": { - "@yarnpkg/libzip": "^2.3.0", - "tslib": "^1.13.0" - }, - "engines": { - "node": ">=12 <14 || 14.2 - 14.9 || >14.10.0" - } - }, - "node_modules/@yarnpkg/fslib/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/@yarnpkg/libzip": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.3.0.tgz", - "integrity": "sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==", - "dev": true, - "dependencies": { - "@types/emscripten": "^1.39.6", - "tslib": "^1.13.0" - }, - "engines": { - "node": ">=12 <14 || 14.2 - 14.9 || >14.10.0" - } - }, - "node_modules/@yarnpkg/libzip/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -18286,15 +16309,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", @@ -18302,15 +16316,6 @@ "dev": true, "license": "MIT" }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/adm-zip": { "version": "0.5.9", "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz", @@ -18572,12 +16577,6 @@ "node": ">= 8" } }, - "node_modules/app-root-dir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", - "integrity": "sha512-jlpIfsOoNoafl92Sz//64uQHGSyMrD2vYG5d8o2a4qGvyNCvXur7bzIsWtAC/6flI2RYAp3kv8rsfBtaLm7w0g==", - "dev": true - }, "node_modules/app-root-path": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz", @@ -19412,6 +17411,15 @@ "inherits": "2.0.3" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -19542,43 +17550,6 @@ "integrity": "sha512-LEeSAWeh2Gfa2FtlQE1shxQ8zi5F9GHarrGKz08TMdODD5T4eH6BMsvtnhbWZ+XQn+Gb6om/917ucvRu7l7ukw==", "dev": true }, - "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/autoprefixer/node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, "node_modules/autosize": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.2.tgz", @@ -19827,12 +17798,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/babel-plugin-add-react-displayname": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", - "integrity": "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw==", - "dev": true - }, "node_modules/babel-plugin-inline-json-import": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/babel-plugin-inline-json-import/-/babel-plugin-inline-json-import-0.3.2.tgz", @@ -20043,27 +18008,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/babel-runtime": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.25.0.tgz", - "integrity": "sha512-zeCYxDePWYAT/DfmQWIHsMSFW2vv45UIwIAMjGvQVsTd47RwsiRH0uK1yzyWZ7LDBKdhnGDPM6NYEO5CZyhPrg==", - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.10.0" - } - }, - "node_modules/babel-runtime/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true - }, - "node_modules/babel-runtime/node_modules/regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha512-02YopEIhAgiBHWeoTiA8aitHDt8z6w+rQqNuIftlM+ZtvSl/brTouaU7DW6GO/cHtvxJvS4Hwv2ibKdxIRi24w==" - }, "node_modules/bail": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.3.tgz", @@ -21108,6 +19052,22 @@ "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.0.3.tgz", "integrity": "sha512-Jt9tIBkRc9POUof7QA/VwWd+58fKkEEfI+/t1/eOlxKM7ZhrczNzMFefge7Ai+39y1pR/pP6cI19guHy3FSLmw==" }, + "node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -21194,6 +19154,15 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/check-node-version": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-4.1.0.tgz", @@ -21466,24 +19435,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "dev": true, - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/citty/node_modules/consola": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", - "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", - "dev": true, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -21574,50 +19525,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cli-table3/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/cli-table3/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-table3/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -21679,9 +19586,13 @@ } }, "node_modules/cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } }, "node_modules/client-zip": { "version": "2.4.5", @@ -24010,6 +21921,15 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -24386,12 +22306,6 @@ "node": ">= 0.4" } }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "dev": true - }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -24610,133 +22524,6 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, - "node_modules/detect-package-manager": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-package-manager/-/detect-package-manager-2.0.1.tgz", - "integrity": "sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==", - "dev": true, - "dependencies": { - "execa": "^5.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/detect-package-manager/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/detect-package-manager/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/detect-package-manager/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/detect-package-manager/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/detect-package-manager/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-package-manager/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-package-manager/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/detect-package-manager/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/detect-port": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz", - "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==", - "dev": true, - "dependencies": { - "address": "^1.0.1", - "debug": "4" - }, - "bin": { - "detect": "bin/detect-port.js", - "detect-port": "bin/detect-port.js" - } - }, "node_modules/devtools-protocol": { "version": "0.0.1367902", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", @@ -24938,15 +22725,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/dset": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", @@ -25506,9 +23284,9 @@ "dev": true }, "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" }, "node_modules/es-set-tostringtag": { "version": "2.0.1", @@ -25547,6 +23325,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.29.0.tgz", + "integrity": "sha512-GjTll+E6APcfAQA09D89HdT8Qn2Yb+TeDSDBTMcxAo+V+w1amAtCI15LJu4YPH/UCPoSo/F47Gr1LIM0TE0lZA==", + "dev": true + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -25591,16 +23375,10 @@ "@esbuild/win32-x64": "0.18.20" } }, - "node_modules/esbuild-plugin-alias": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz", - "integrity": "sha512-jyfL/pwPqaFXyKnj8lP8iLk6Z0m099uXR45aSN8Av1XD4vhvQutxxPzgA2bTcAwQpa1zCXDcWOlhFgyP3GKqhQ==", - "dev": true - }, "node_modules/esbuild-register": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", - "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -26452,6 +24230,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", @@ -26862,9 +24649,10 @@ } }, "node_modules/external-editor": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz", - "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -27159,12 +24947,6 @@ "node": "^12.20 || >= 14.13" } }, - "node_modules/fetch-retry": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", - "integrity": "sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==", - "dev": true - }, "node_modules/figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -27195,51 +24977,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-system-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", - "integrity": "sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==", - "dev": true, - "dependencies": { - "fs-extra": "11.1.1", - "ramda": "0.29.0" - } - }, - "node_modules/file-system-cache/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/file-system-cache/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/file-system-cache/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/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -27907,6 +25644,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, "engines": { "node": "*" }, @@ -28362,15 +26100,6 @@ "node": ">=6" } }, - "node_modules/get-npm-tarball-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/get-npm-tarball-url/-/get-npm-tarball-url-2.1.0.tgz", - "integrity": "sha512-ro+DiMu5DXgRBabqXupW38h7WPZ9+Ad8UjwhvsmmN8w1sU7ab0nzAXvVZ4kqYg57OrqomRtJvepX5/xvFKNtjA==", - "dev": true, - "engines": { - "node": ">=12.17" - } - }, "node_modules/get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", @@ -28580,34 +26309,6 @@ "safe-buffer": "^5.1.1" } }, - "node_modules/giget": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.1.tgz", - "integrity": "sha512-4VG22mopWtIeHwogGSy1FViXVo0YT+m6BrqZfz0JJFwbSsePsCdOzdLIIli5BtMp7Xe8f/o2OmBpQX2NBOC24g==", - "dev": true, - "dependencies": { - "citty": "^0.1.5", - "consola": "^3.2.3", - "defu": "^6.1.3", - "node-fetch-native": "^1.6.1", - "nypm": "^0.3.3", - "ohash": "^1.1.3", - "pathe": "^1.1.1", - "tar": "^6.2.0" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, - "node_modules/giget/node_modules/consola": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", - "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", - "dev": true, - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/git-raw-commits": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-3.0.0.tgz", @@ -28706,12 +26407,6 @@ "license": "MIT", "optional": true }, - "node_modules/github-slugger": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.4.0.tgz", - "integrity": "sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==", - "dev": true - }, "node_modules/glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", @@ -28945,38 +26640,6 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, - "node_modules/gunzip-maybe": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", - "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", - "dev": true, - "dependencies": { - "browserify-zlib": "^0.1.4", - "is-deflate": "^1.0.0", - "is-gzip": "^1.0.0", - "peek-stream": "^1.1.0", - "pumpify": "^1.3.3", - "through2": "^2.0.3" - }, - "bin": { - "gunzip-maybe": "bin.js" - } - }, - "node_modules/gunzip-maybe/node_modules/browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", - "dev": true, - "dependencies": { - "pako": "~0.2.0" - } - }, - "node_modules/gunzip-maybe/node_modules/pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", - "dev": true - }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -29430,9 +27093,9 @@ } }, "node_modules/html-webpack-plugin": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", - "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.3.tgz", + "integrity": "sha512-QSf1yjtSAsmf7rYBV7XX86uua4W/vkhIt0xNXKbsi2foEeW7vjJQz4bhnpL3xH+l1ryl1680uNv968Z+X6jSYg==", "dev": true, "dependencies": { "@types/html-minifier-terser": "^6.0.0", @@ -30083,145 +27746,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/inquirer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", - "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.15", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.5.3", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/inquirer/node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inquirer/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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/inquirer/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inquirer/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/inquirer/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==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -30303,15 +27827,6 @@ "node": ">=8" } }, - "node_modules/is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-accessor-descriptor": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", @@ -30511,12 +28026,6 @@ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.2.tgz", "integrity": "sha512-TRzl7mOCchnhchN+f3ICUCzYvL9ul7R+TYOsZ8xia++knyZAJfv/uA1FvQXsAnYIl1T3B2X5E/J7Wb1QXiIBXg==" }, - "node_modules/is-deflate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", - "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", - "dev": true - }, "node_modules/is-descriptor": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", @@ -30610,15 +28119,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-gzip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", - "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-hexadecimal": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz", @@ -30679,22 +28179,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -32718,20 +30202,6 @@ "node": ">=0.10.0" } }, - "node_modules/lazy-universal-dotenv": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz", - "integrity": "sha512-aXpZJRnTkpK6gQ/z4nk+ZBLd/Qdp118cvPruLSIQzQNRhKwEcdXCOzXuF55VDqIiuAaY3UGZ10DJtvZzDcvsxg==", - "dev": true, - "dependencies": { - "app-root-dir": "^1.0.2", - "dotenv": "^16.0.0", - "dotenv-expand": "^10.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -35045,6 +32515,12 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -35092,15 +32568,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "version": "0.30.15", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", + "integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-dir": { @@ -35627,18 +33100,6 @@ "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==" }, - "node_modules/markdown-to-jsx": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.4.1.tgz", - "integrity": "sha512-GbrbkTnHp9u6+HqbPRFJbObi369AgJNXi/sGqq5HRsoZW063xR1XDCaConqq+whfEIAlzB1YPnOgsPc7B7bc/A==", - "dev": true, - "engines": { - "node": ">= 10" - }, - "peerDependencies": { - "react": ">= 0.14.0" - } - }, "node_modules/markdownlint": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.25.1.tgz", @@ -35812,58 +33273,6 @@ "unist-util-visit": "^1.1.0" } }, - "node_modules/mdast-util-definitions": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", - "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", - "dev": true, - "dependencies": { - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mdast-util-inject": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mdast-util-inject/-/mdast-util-inject-1.1.0.tgz", @@ -37442,15 +34851,16 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -37640,12 +35050,6 @@ } } }, - "node_modules/node-fetch-native": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.2.tgz", - "integrity": "sha512-69mtXOFZ6hSkYiXAVB5SqaRvrbITC/NPyqv7yuu/qw0nmgPyYbIMYYNIDhNtwPrzk0ptrimrLz/hhjvm4w5Z+w==", - "dev": true - }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -38923,158 +36327,6 @@ "node": ">=12" } }, - "node_modules/nypm": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.6.tgz", - "integrity": "sha512-2CATJh3pd6CyNfU5VZM7qSwFu0ieyabkEdnogE30Obn1czrmOYiZ8DOZLe1yBdLKWoyD3Mcy2maUs+0MR3yVjQ==", - "dev": true, - "dependencies": { - "citty": "^0.1.5", - "execa": "^8.0.1", - "pathe": "^1.1.2", - "ufo": "^1.3.2" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, - "node_modules/nypm/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/nypm/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/nypm/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nypm/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/nypm/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ob1": { "version": "0.80.5", "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.80.5.tgz", @@ -39281,12 +36533,6 @@ "integrity": "sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==", "dev": true }, - "node_modules/ohash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", - "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", - "dev": true - }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -40520,11 +37766,14 @@ "node": ">=4" } }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } }, "node_modules/pbkdf2": { "version": "3.1.2", @@ -40542,17 +37791,6 @@ "node": ">=0.12" } }, - "node_modules/peek-stream": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", - "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "duplexify": "^3.5.0", - "through2": "^2.0.3" - } - }, "node_modules/pegjs": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz", @@ -41569,15 +38807,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, - "node_modules/pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", @@ -42125,16 +39354,6 @@ "node": ">=8" } }, - "node_modules/ramda": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.29.0.tgz", - "integrity": "sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -42338,9 +39557,9 @@ } }, "node_modules/react-docgen": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.0.3.tgz", - "integrity": "sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.0.tgz", + "integrity": "sha512-APPU8HB2uZnpl6Vt/+0AFoVYgSRtfiP6FLrZgPPTDmqSb2R4qZRbgd0A3VzIFxDt5e+Fozjx79WjLWnF69DK8g==", "dev": true, "dependencies": { "@babel/core": "^7.18.9", @@ -42368,12 +39587,6 @@ "typescript": ">= 4.3.x" } }, - "node_modules/react-docgen/node_modules/@types/doctrine": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", - "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", - "dev": true - }, "node_modules/react-docgen/node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -42413,27 +39626,6 @@ "react": "^18.3.1" } }, - "node_modules/react-element-to-jsx-string": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz", - "integrity": "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==", - "dev": true, - "dependencies": { - "@base2/pretty-print-object": "1.0.1", - "is-plain-object": "5.0.0", - "react-is": "18.1.0" - }, - "peerDependencies": { - "react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0", - "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0" - } - }, - "node_modules/react-element-to-jsx-string/node_modules/react-is": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", - "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", - "dev": true - }, "node_modules/react-freeze": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.3.tgz", @@ -42856,9 +40048,9 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -43526,62 +40718,6 @@ "unified": "^7.0.0" } }, - "node_modules/remark-external-links": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/remark-external-links/-/remark-external-links-8.0.0.tgz", - "integrity": "sha512-5vPSX0kHoSsqtdftSHhIYofVINC8qmp0nctkeU9YoJwV3YfiBRiI6cbFRJ0oI/1F9xS+bopXG0m2KS8VFscuKA==", - "dev": true, - "dependencies": { - "extend": "^3.0.0", - "is-absolute-url": "^3.0.0", - "mdast-util-definitions": "^4.0.0", - "space-separated-tokens": "^1.0.0", - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-external-links/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/remark-parse": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-6.0.3.tgz", @@ -43604,60 +40740,6 @@ "xtend": "^4.0.1" } }, - "node_modules/remark-slug": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/remark-slug/-/remark-slug-6.1.0.tgz", - "integrity": "sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==", - "dev": true, - "dependencies": { - "github-slugger": "^1.0.0", - "mdast-util-to-string": "^1.0.0", - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-slug/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-slug/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-slug/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "dev": true, - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/remark-stringify": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz", @@ -44146,14 +41228,14 @@ "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" }, "node_modules/rtlcss": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.0.0.tgz", - "integrity": "sha512-j6oypPP+mgFwDXL1JkLCtm6U/DQntMUqlv5SOhpgHhdIE+PmBcjrtAHIpXfbIup47kD5Sgja9JDsDF1NNOsBwQ==", - "dev": true, + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0", - "postcss": "^8.4.6", + "postcss": "^8.4.21", "strip-json-comments": "^3.1.1" }, "bin": { @@ -44163,96 +41245,10 @@ "node": ">=12.0.0" } }, - "node_modules/rtlcss-webpack-plugin": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/rtlcss-webpack-plugin/-/rtlcss-webpack-plugin-4.0.7.tgz", - "integrity": "sha512-ouSbJtgcLBBQIsMgarxsDnfgRqm/AS4BKls/mz/Xb6HSl+PdEzefTR+Wz5uWQx4odoX0g261Z7yb3QBz0MTm0g==", - "dependencies": { - "babel-runtime": "~6.25.0", - "rtlcss": "^3.5.0" - } - }, - "node_modules/rtlcss-webpack-plugin/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rtlcss-webpack-plugin/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rtlcss-webpack-plugin/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rtlcss-webpack-plugin/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/rtlcss-webpack-plugin/node_modules/rtlcss": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-3.5.0.tgz", - "integrity": "sha512-wzgMaMFHQTnyi9YOwsx9LjOxYXJPzS8sYnFaKm6R5ysvTkwzHiB0vxnbHwchHQT65PTdBjDG21/kQBWI7q9O7A==", - "dependencies": { - "find-up": "^5.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.3.11", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "rtlcss": "bin/rtlcss.js" - } - }, - "node_modules/rtlcss-webpack-plugin/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/rtlcss/node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -44371,6 +41367,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -44449,6 +41446,7 @@ "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, "dependencies": { "tslib": "^1.9.0" }, @@ -44459,7 +41457,8 @@ "node_modules/rxjs/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/sade": { "version": "1.8.1", @@ -44553,9 +41552,9 @@ } }, "node_modules/sass": { - "version": "1.50.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.50.1.tgz", - "integrity": "sha512-noTnY41KnlW2A9P8sdwESpDmo+KBNkukI1i8+hOK3footBUcohNHtdOJbckp46XO95nuvcHDDZ+4tmOnpK3hjw==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.0.tgz", + "integrity": "sha512-C4zp79GCXZfK0yoHZg+GxF818/aclhp9F48XBu/+bm9vXEVAYov9iU3FBVRMq3Hx3OA4jfKL+p2K9180mEh0xQ==", "license": "MIT", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -45807,16 +42806,6 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "deprecated": "See https://github.com/lydell/source-map-url#deprecated" }, - "node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/spacetrim": { "version": "0.11.59", "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.59.tgz", @@ -46130,27 +43119,30 @@ "node": ">= 0.4" } }, - "node_modules/store2": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.2.tgz", - "integrity": "sha512-siT1RiqlfQnGqgT/YzXVUNsom9S0H1OX+dpdGN1xkyYATo4I6sep5NmsRD/40s3IIOvlCq6akxkqG82urIZW1w==", - "dev": true - }, "node_modules/storybook": { - "version": "7.6.15", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-7.6.15.tgz", - "integrity": "sha512-Ybezq9JRk5CBhzjgzZ/oT7mnU45UwhyVSGKW+PUKZGGUG9VH2hCrTEES9f/zEF82kj/5COVPyqR/5vlXuuS39A==", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.4.7.tgz", + "integrity": "sha512-RP/nMJxiWyFc8EVMH5gp20ID032Wvk+Yr3lmKidoegto5Iy+2dVQnUoElZb2zpbVXNHWakGuAkfI0dY1Hfp/vw==", "dev": true, "dependencies": { - "@storybook/cli": "7.6.15" + "@storybook/core": "8.4.7" }, "bin": { - "sb": "index.js", - "storybook": "index.js" + "getstorybook": "bin/index.cjs", + "sb": "bin/index.cjs", + "storybook": "bin/index.cjs" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } } }, "node_modules/storybook-source-link": { @@ -47102,19 +44094,6 @@ "node": ">=0.10.0" } }, - "node_modules/swc-loader": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", - "integrity": "sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg==", - "dev": true, - "dependencies": { - "@swc/counter": "^0.1.3" - }, - "peerDependencies": { - "@swc/core": "^1.2.147", - "webpack": ">=2" - } - }, "node_modules/symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -47129,12 +44108,6 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, - "node_modules/synchronous-promise": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", - "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==", - "dev": true - }, "node_modules/synckit": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", @@ -47255,6 +44228,7 @@ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", "dev": true, + "optional": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -47268,6 +44242,7 @@ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -47387,15 +44362,6 @@ "node": ">= 8" } }, - "node_modules/telejson": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz", - "integrity": "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==", - "dev": true, - "dependencies": { - "memoizerific": "^1.11.3" - } - }, "node_modules/temp": { "version": "0.8.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", @@ -47447,128 +44413,6 @@ "rimraf": "bin.js" } }, - "node_modules/tempy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", - "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", - "dev": true, - "dependencies": { - "del": "^6.0.0", - "is-stream": "^2.0.0", - "temp-dir": "^2.0.0", - "type-fest": "^0.16.0", - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dev": true, - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tempy/node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tempy/node_modules/temp-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", - "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", - "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/terminal-link": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.0.0.tgz", @@ -47829,6 +44673,24 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "dev": true }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -47940,12 +44802,6 @@ "node": ">=0.10.0" } }, - "node_modules/tocbot": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/tocbot/-/tocbot-4.25.0.tgz", - "integrity": "sha512-kE5wyCQJ40hqUaRVkyQ4z5+4juzYsv/eK+aqD97N62YH0TxFhzJvo22RUQQZdO3YnXAk42ZOfOpjVdy+Z0YokA==", - "dev": true - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -48409,12 +45265,6 @@ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, - "node_modules/ufo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", - "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", - "dev": true - }, "node_modules/uglify-js": { "version": "3.13.7", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.7.tgz", @@ -48487,9 +45337,10 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" }, "node_modules/unherit": { "version": "1.1.1", @@ -48688,23 +45539,23 @@ } }, "node_modules/unplugin": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.7.1.tgz", - "integrity": "sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.0.tgz", + "integrity": "sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==", "dev": true, "dependencies": { - "acorn": "^8.11.3", - "chokidar": "^3.5.3", - "webpack-sources": "^3.2.3", - "webpack-virtual-modules": "^0.6.1" + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" } }, "node_modules/unplugin/node_modules/acorn": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", - "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -48712,13 +45563,6 @@ "node": ">=0.4.0" } }, - "node_modules/unplugin/node_modules/webpack-virtual-modules": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", - "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", - "dev": true, - "license": "MIT" - }, "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -48981,19 +45825,6 @@ "react": "^16.8.0" } }, - "node_modules/use-resize-observer": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", - "integrity": "sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==", - "dev": true, - "dependencies": { - "@juggle/resize-observer": "^3.3.1" - }, - "peerDependencies": { - "react": "16.8.0 - 18", - "react-dom": "16.8.0 - 18" - } - }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", @@ -49661,16 +46492,6 @@ "node": ">=14.16" } }, - "node_modules/webdriver/node_modules/@types/node": { - "version": "20.17.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", - "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, "node_modules/webdriver/node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -49831,13 +46652,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webdriver/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "node_modules/webdriver/node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", @@ -49934,16 +46748,6 @@ } } }, - "node_modules/webdriverio/node_modules/@types/node": { - "version": "20.17.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", - "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" - } - }, "node_modules/webdriverio/node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -50191,13 +46995,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/webdriverio/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "license": "MIT" - }, "node_modules/webdriverio/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -50505,7 +47302,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -50542,9 +47338,9 @@ "dev": true }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", @@ -50553,7 +47349,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -50829,9 +47625,9 @@ } }, "node_modules/webpack-virtual-modules": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", - "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", "dev": true }, "node_modules/webpack/node_modules/@types/estree": { @@ -51847,6 +48643,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zip-stream": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.2.tgz", @@ -53883,6 +50691,7 @@ "version": "4.57.0", "license": "GPL-2.0-or-later", "dependencies": { + "@inquirer/prompts": "^7.2.0", "@wordpress/lazy-import": "*", "chalk": "^4.0.0", "change-case": "^4.1.2", @@ -53890,7 +50699,6 @@ "commander": "^9.2.0", "execa": "^4.0.2", "fast-glob": "^3.2.7", - "inquirer": "^7.1.0", "make-dir": "^3.0.0", "mustache": "^4.0.0", "npm-package-arg": "^8.1.5", @@ -54290,6 +51098,7 @@ "@wordpress/icons": "*", "@wordpress/keyboard-shortcuts": "*", "@wordpress/keycodes": "*", + "@wordpress/media-utils": "5.14.0", "@wordpress/notices": "*", "@wordpress/patterns": "*", "@wordpress/plugins": "*", @@ -54448,12 +51257,12 @@ "version": "10.14.0", "license": "GPL-2.0-or-later", "dependencies": { + "@inquirer/prompts": "^7.2.0", "chalk": "^4.0.0", "copy-dir": "^1.3.0", "docker-compose": "^0.24.3", "extract-zip": "^1.6.7", "got": "^11.8.5", - "inquirer": "^7.1.0", "js-yaml": "^3.13.1", "ora": "^4.0.2", "rimraf": "^5.0.10", @@ -55019,7 +51828,7 @@ "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/base-styles": "*", - "autoprefixer": "^10.2.5" + "autoprefixer": "^10.4.20" }, "engines": { "node": ">=18.12.0", @@ -55029,6 +51838,59 @@ "postcss": "^8.0.0" } }, + "packages/postcss-plugins-preset/node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "packages/postcss-plugins-preset/node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "packages/postcss-plugins-preset/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "packages/postcss-themes": { "name": "@wordpress/postcss-themes", "version": "6.14.0", @@ -55498,8 +52360,8 @@ "react-refresh": "^0.14.0", "read-pkg-up": "^7.0.1", "resolve-bin": "^0.4.0", - "rtlcss-webpack-plugin": "^4.0.7", - "sass": "^1.50.1", + "rtlcss": "^4.3.0", + "sass": "^1.54.0", "sass-loader": "^16.0.3", "schema-utils": "^4.2.0", "source-map-loader": "^3.0.0", @@ -55792,6 +52654,28 @@ "npm": ">=8.19.2" } }, + "packages/upload-media": { + "name": "@wordpress/upload-media", + "version": "1.0.0-prerelease", + "license": "GPL-2.0-or-later", + "dependencies": { + "@shopify/web-worker": "^6.4.0", + "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/blob": "file:../blob", + "@wordpress/compose": "file:../compose", + "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/preferences": "file:../preferences", + "@wordpress/private-apis": "file:../private-apis", + "@wordpress/url": "file:../url", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } + }, "packages/url": { "name": "@wordpress/url", "version": "4.14.0", diff --git a/package.json b/package.json index dbf69043c58292..58a99ada7308ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "19.9.0-rc.1", + "version": "20.0.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -31,9 +31,11 @@ "@babel/runtime-corejs3": "7.25.7", "@babel/traverse": "7.25.7", "@emotion/babel-plugin": "11.11.0", + "@emotion/is-prop-valid": "1.2.2", "@emotion/jest": "11.7.1", "@emotion/native": "11.0.0", - "@geometricpanda/storybook-addon-badges": "2.0.1", + "@geometricpanda/storybook-addon-badges": "2.0.5", + "@inquirer/prompts": "7.2.0", "@octokit/rest": "16.26.0", "@octokit/types": "6.34.0", "@octokit/webhooks-types": "5.8.0", @@ -42,16 +44,19 @@ "@react-native/babel-preset": "0.73.10", "@react-native/metro-babel-transformer": "0.73.10", "@react-native/metro-config": "0.73.4", - "@storybook/addon-a11y": "7.6.15", - "@storybook/addon-actions": "7.6.15", - "@storybook/addon-controls": "7.6.15", - "@storybook/addon-docs": "7.6.15", - "@storybook/addon-toolbars": "7.6.15", - "@storybook/addon-viewport": "7.6.15", - "@storybook/react": "7.6.15", - "@storybook/react-webpack5": "7.6.15", - "@storybook/source-loader": "7.6.15", - "@storybook/theming": "7.6.15", + "@storybook/addon-a11y": "8.4.7", + "@storybook/addon-actions": "8.4.7", + "@storybook/addon-controls": "8.4.7", + "@storybook/addon-docs": "8.4.7", + "@storybook/addon-toolbars": "8.4.7", + "@storybook/addon-viewport": "8.4.7", + "@storybook/addon-webpack5-compiler-babel": "3.0.3", + "@storybook/react": "8.4.7", + "@storybook/react-webpack5": "8.4.7", + "@storybook/source-loader": "8.4.7", + "@storybook/test": "8.4.7", + "@storybook/theming": "8.4.7", + "@storybook/types": "8.4.7", "@testing-library/jest-dom": "5.16.5", "@testing-library/react": "14.3.0", "@testing-library/react-native": "12.4.3", @@ -60,6 +65,7 @@ "@types/estree": "1.0.5", "@types/istanbul-lib-report": "3.0.0", "@types/mime": "2.0.3", + "@types/node": "20.17.10", "@types/npm-package-arg": "6.1.1", "@types/prettier": "2.4.4", "@types/qs": "6.9.7", @@ -110,7 +116,6 @@ "filenamify": "4.2.0", "glob": "7.1.2", "husky": "7.0.0", - "inquirer": "7.1.0", "jest": "29.6.2", "jest-environment-jsdom": "^29.6.2", "jest-jasmine2": "29.6.2", @@ -148,15 +153,15 @@ "redux": "5.0.1", "resize-observer-polyfill": "1.5.1", "rimraf": "5.0.10", - "rtlcss": "4.0.0", - "sass": "1.50.1", + "rtlcss": "4.3.0", + "sass": "1.54.0", "sass-loader": "16.0.3", "semver": "7.5.4", "simple-git": "3.24.0", "snapshot-diff": "0.10.0", "source-map-loader": "3.0.0", "sprintf-js": "1.1.1", - "storybook": "7.6.15", + "storybook": "8.4.7", "storybook-source-link": "2.0.9", "strip-json-comments": "5.0.0", "style-loader": "3.2.1", @@ -180,8 +185,8 @@ "build:packages": "npm run --silent build:package-types && node ./bin/packages/build.js", "postbuild:packages": " npm run --if-present --workspaces build:wp", "build:plugin-zip": "bash ./bin/build-plugin-zip.sh", - "clean:package-types": "tsc --build --clean && rimraf \"./packages/*/build-types\"", - "clean:packages": "rimraf \"./packages/*/@(build|build-module|build-wp|build-style)\"", + "clean:package-types": "tsc --build --clean && rimraf --glob \"./packages/*/build-types\"", + "clean:packages": "rimraf --glob \"./packages/*/{build,build-module,build-wp,build-style}\"", "component-usage-stats": "node ./node_modules/react-scanner/bin/react-scanner -c ./react-scanner.config.js", "dev": "cross-env NODE_ENV=development npm run build:packages && concurrently \"wp-scripts start\" \"npm run dev:packages\"", "dev:packages": "cross-env NODE_ENV=development concurrently \"node ./bin/packages/watch.js\" \"tsc --build --watch\"", @@ -193,7 +198,7 @@ "docs:gen": "node ./docs/tool/index.js", "docs:theme-ref": "node ./bin/api-docs/gen-theme-reference.mjs", "env": "wp-env", - "fixtures:clean": "rimraf \"test/integration/fixtures/blocks/*.+(json|serialized.html)\"", + "fixtures:clean": "rimraf --glob \"test/integration/fixtures/blocks/*.{json,serialized.html}\"", "fixtures:generate": "cross-env GENERATE_MISSING_FIXTURES=y npm run test:unit test/integration/full-content/ && npm run format test/integration/fixtures/blocks/*.json", "fixtures:regenerate": "npm-run-all fixtures:clean fixtures:generate", "format": "wp-scripts format", diff --git a/packages/a11y/tsconfig.json b/packages/a11y/tsconfig.json index 093c2775f96d66..13229eadde8f21 100644 --- a/packages/a11y/tsconfig.json +++ b/packages/a11y/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "references": [ { "path": "../dom-ready" }, { "path": "../i18n" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../dom-ready" }, { "path": "../i18n" } ] } diff --git a/packages/api-fetch/tsconfig.json b/packages/api-fetch/tsconfig.json index f9d517286a102f..635fe4a8c0d353 100644 --- a/packages/api-fetch/tsconfig.json +++ b/packages/api-fetch/tsconfig.json @@ -1,11 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, "references": [ { "path": "../i18n" }, { "path": "../url" } ], - "include": [ "src/**/*" ], - "exclude": [ "**/test/**/*" ] + "exclude": [ "**/test" ] } diff --git a/packages/autop/tsconfig.json b/packages/autop/tsconfig.json index a09ec7466c435b..f68a855bab79cc 100644 --- a/packages/autop/tsconfig.json +++ b/packages/autop/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "references": [ { "path": "../dom-ready" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../dom-ready" } ] } diff --git a/packages/blob/tsconfig.json b/packages/blob/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/blob/tsconfig.json +++ b/packages/blob/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 13dffce114f59a..8fe2c5f1179dcd 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -713,10 +713,50 @@ Undocumented declaration. ### PlainText +Render an auto-growing textarea allow users to fill any textual content. + _Related_ - +_Usage_ + +```jsx +import { registerBlockType } from '@wordpress/blocks'; +import { PlainText } from '@wordpress/block-editor'; + +registerBlockType( 'my-plugin/example-block', { + // ... + + attributes: { + content: { + type: 'string', + }, + }, + + edit( { className, attributes, setAttributes } ) { + return ( + setAttributes( { content } ) } + /> + ); + }, +} ); +``` + +_Parameters_ + +- _props_ `Object`: Component props. +- _props.value_ `string`: String value of the textarea. +- _props.onChange_ `Function`: Function called when the text value changes. +- _props.ref_ `[Object]`: The component forwards the `ref` property to the `TextareaAutosize` component. + +_Returns_ + +- `Element`: Plain text component + ### privateApis Private @wordpress/block-editor APIs. diff --git a/packages/block-editor/src/components/audio-player/index.native.js b/packages/block-editor/src/components/audio-player/index.native.js index bee31ea5872ef5..734226408cb923 100644 --- a/packages/block-editor/src/components/audio-player/index.native.js +++ b/packages/block-editor/src/components/audio-player/index.native.js @@ -17,7 +17,7 @@ import { View } from '@wordpress/primitives'; import { Icon } from '@wordpress/components'; import { withPreferredColorScheme } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; -import { audio, warning } from '@wordpress/icons'; +import { audio, cautionFilled } from '@wordpress/icons'; import { requestImageFailedRetryDialog, requestImageUploadCancelDialog, @@ -167,7 +167,7 @@ function Player( { <View style={ styles.subtitleContainer }> { isUploadFailed && ( <Icon - icon={ warning } + icon={ cautionFilled } style={ { ...styles.errorIcon, ...uploadFailedStyle, diff --git a/packages/block-editor/src/components/background-image-control/index.js b/packages/block-editor/src/components/background-image-control/index.js index 2703aa3988d64e..6c703ad2eadb4d 100644 --- a/packages/block-editor/src/components/background-image-control/index.js +++ b/packages/block-editor/src/components/background-image-control/index.js @@ -24,6 +24,7 @@ import { Placeholder, Spinner, __experimentalDropdownContentWrapper as DropdownContentWrapper, + Button, } from '@wordpress/components'; import { __, _x, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; @@ -378,6 +379,9 @@ function BackgroundImageControls( { /> } variant="secondary" + renderToggle={ ( props ) => ( + <Button { ...props } __next40pxDefaultSize /> + ) } onError={ onUploadError } onReset={ () => { closeAndFocus(); diff --git a/packages/block-editor/src/components/background-image-control/style.scss b/packages/block-editor/src/components/background-image-control/style.scss index cde8044c24c121..b9c94916039c44 100644 --- a/packages/block-editor/src/components/background-image-control/style.scss +++ b/packages/block-editor/src/components/background-image-control/style.scss @@ -23,7 +23,10 @@ .components-dropdown { display: block; - height: 36px; + + .block-editor-global-styles-background-panel__dropdown-toggle { + height: 40px; + } } } @@ -44,7 +47,6 @@ .components-dropdown { display: block; - height: 36px; } button.components-button { diff --git a/packages/block-editor/src/components/block-alignment-matrix-control/README.md b/packages/block-editor/src/components/block-alignment-matrix-control/README.md index dfb38e15964124..b4267d68fe1fdc 100644 --- a/packages/block-editor/src/components/block-alignment-matrix-control/README.md +++ b/packages/block-editor/src/components/block-alignment-matrix-control/README.md @@ -41,13 +41,36 @@ const controls = ( /> </BlockControls> </> -} +); ``` ### Props -| Name | Type | Default | Description | -| ---------- | ---------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `label` | `string` | `Change matrix alignment` | concise description of tool's functionality. | -| `onChange` | `function` | `noop` | the function to execute upon a user's change of the matrix state | -| `value` | `string` | `center` | describes the content alignment location and can be `top`, `right`, `bottom`, `left`, `topRight`, `bottomRight`, `bottomLeft`, `topLeft` | +### `label` + +- **Type:** `string` +- **Default:** `'Change matrix alignment'` + +Label for the control. + +### `onChange` + +- **Type:** `Function` +- **Default:** `noop` + +Function to execute upon a user's change of the matrix state. + +### `value` + +- **Type:** `string` +- **Default:** `'center'` +- **Options:** `'center'`, `'center center'`, `'center left'`, `'center right'`, `'top center'`, `'top left'`, `'top right'`, `'bottom center'`, `'bottom left'`, `'bottom right'` + +Content alignment location. + +### `isDisabled` + +- **Type:** `boolean` +- **Default:** `false` + +Whether the control should be disabled. \ No newline at end of file diff --git a/packages/block-editor/src/components/block-alignment-matrix-control/index.js b/packages/block-editor/src/components/block-alignment-matrix-control/index.js index cdec41dfc7b978..fef7b424fdc947 100644 --- a/packages/block-editor/src/components/block-alignment-matrix-control/index.js +++ b/packages/block-editor/src/components/block-alignment-matrix-control/index.js @@ -11,6 +11,37 @@ import { const noop = () => {}; +/** + * The alignment matrix control allows users to quickly adjust inner block alignment. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-alignment-matrix-control/README.md + * + * @example + * ```jsx + * function Example() { + * return ( + * <BlockControls> + * <BlockAlignmentMatrixControl + * label={ __( 'Change content position' ) } + * value="center" + * onChange={ ( nextPosition ) => + * setAttributes( { contentPosition: nextPosition } ) + * } + * /> + * </BlockControls> + * ); + * } + * ``` + * + * @param {Object} props Component props. + * @param {string} props.label Label for the control. Defaults to 'Change matrix alignment'. + * @param {Function} props.onChange Function to execute upon change of matrix state. + * @param {string} props.value Content alignment location. One of: 'center', 'center center', + * 'center left', 'center right', 'top center', 'top left', + * 'top right', 'bottom center', 'bottom left', 'bottom right'. + * @param {boolean} props.isDisabled Whether the control should be disabled. + * @return {Element} The BlockAlignmentMatrixControl component. + */ function BlockAlignmentMatrixControl( props ) { const { label = __( 'Change matrix alignment' ), diff --git a/packages/block-editor/src/components/block-alignment-matrix-control/stories/index.story.js b/packages/block-editor/src/components/block-alignment-matrix-control/stories/index.story.js new file mode 100644 index 00000000000000..c2e1d27ea55b9f --- /dev/null +++ b/packages/block-editor/src/components/block-alignment-matrix-control/stories/index.story.js @@ -0,0 +1,78 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import BlockAlignmentMatrixControl from '../'; + +const meta = { + title: 'BlockEditor/BlockAlignmentMatrixControl', + component: BlockAlignmentMatrixControl, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: + 'Renders a control for selecting block alignment using a matrix of alignment options.', + }, + }, + }, + argTypes: { + label: { + control: 'text', + table: { + type: { summary: 'string' }, + defaultValue: { summary: "'Change matrix alignment'" }, + }, + description: 'Label for the control.', + }, + onChange: { + action: 'onChange', + control: { type: null }, + table: { + type: { summary: 'function' }, + defaultValue: { summary: '() => {}' }, + }, + description: + "Function to execute upon a user's change of the matrix state.", + }, + isDisabled: { + control: 'boolean', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' }, + }, + description: 'Whether the control should be disabled.', + }, + value: { + control: { type: null }, + table: { + type: { summary: 'string' }, + defaultValue: { summary: "'center'" }, + }, + description: 'Content alignment location.', + }, + }, +}; + +export default meta; + +export const Default = { + render: function Template( { onChange, ...args } ) { + const [ value, setValue ] = useState(); + + return ( + <BlockAlignmentMatrixControl + { ...args } + value={ value } + onChange={ ( ...changeArgs ) => { + onChange( ...changeArgs ); + setValue( ...changeArgs ); + } } + /> + ); + }, +}; diff --git a/packages/block-editor/src/components/block-card/README.md b/packages/block-editor/src/components/block-card/README.md index 216cf4e3865a04..79a42bc20df74a 100644 --- a/packages/block-editor/src/components/block-card/README.md +++ b/packages/block-editor/src/components/block-card/README.md @@ -21,6 +21,7 @@ const MyBlockCard = () => ( icon={ paragraph } title="Paragraph" description="Start with the basic building block of all narrative." + name="Custom Block" /> ); ``` @@ -45,6 +46,12 @@ The title of the block. The description of the block. +#### name + +- **Type:** `String` + +The custom name of the block. + ## Related components Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js index c8a12a3be5ef6a..525a594702e301 100644 --- a/packages/block-editor/src/components/block-card/index.js +++ b/packages/block-editor/src/components/block-card/index.js @@ -6,22 +6,55 @@ import clsx from 'clsx'; /** * WordPress dependencies */ -import deprecated from '@wordpress/deprecated'; import { Button, __experimentalText as Text, __experimentalVStack as VStack, + privateApis as componentsPrivateApis, } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import deprecated from '@wordpress/deprecated'; +import { __, isRTL } from '@wordpress/i18n'; import { chevronLeft, chevronRight } from '@wordpress/icons'; -import { __, _x, isRTL, sprintf } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import BlockIcon from '../block-icon'; +import { unlock } from '../../lock-unlock'; import { store as blockEditorStore } from '../../store'; +import BlockIcon from '../block-icon'; +const { Badge } = unlock( componentsPrivateApis ); + +/** + * A card component that displays block information including title, icon, and description. + * Can be used to show block metadata and navigation controls for parent blocks. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-card/README.md + * + * @example + * ```jsx + * function Example() { + * return ( + * <BlockCard + * title="My Block" + * icon="smiley" + * description="A simple block example" + * name="Custom Block" + * /> + * ); + * } + * ``` + * + * @param {Object} props Component props. + * @param {string} props.title The title of the block. + * @param {string|Object} props.icon The icon of the block. This can be any of [WordPress' Dashicons](https://developer.wordpress.org/resource/dashicons/), or a custom `svg` element. + * @param {string} props.description The description of the block. + * @param {Object} [props.blockType] Deprecated: Object containing block type data. + * @param {string} [props.className] Additional classes to apply to the card. + * @param {string} [props.name] Custom block name to display before the title. + * @return {Element} Block card component. + */ function BlockCard( { title, icon, description, blockType, className, name } ) { if ( blockType ) { deprecated( '`blockType` property in `BlockCard component`', { @@ -66,14 +99,10 @@ function BlockCard( { title, icon, description, blockType, className, name } ) { <BlockIcon icon={ icon } showColors /> <VStack spacing={ 1 }> <h2 className="block-editor-block-card__title"> - { name?.length - ? sprintf( - // translators: 1: Custom block name. 2: Block title. - _x( '%1$s (%2$s)', 'block label' ), - name, - title - ) - : title } + <span className="block-editor-block-card__name"> + { !! name?.length ? name : title } + </span> + { !! name?.length && <Badge>{ title }</Badge> } </h2> { description && ( <Text className="block-editor-block-card__description"> diff --git a/packages/block-editor/src/components/block-card/stories/index.story.js b/packages/block-editor/src/components/block-card/stories/index.story.js new file mode 100644 index 00000000000000..0fe68e2032d394 --- /dev/null +++ b/packages/block-editor/src/components/block-card/stories/index.story.js @@ -0,0 +1,79 @@ +/** + * WordPress dependencies + */ +import { box, button, cog, paragraph } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import BlockCard from '../'; + +const meta = { + title: 'BlockEditor/BlockCard', + component: BlockCard, + parameters: { + docs: { + description: { + component: + 'The `BlockCard` component allows to display a "card" which contains the title of a block, its icon and its description.', + }, + canvas: { sourceState: 'shown' }, + }, + }, + argTypes: { + title: { + control: 'text', + description: 'The title of the block.', + table: { + type: { summary: 'string' }, + }, + }, + description: { + control: 'text', + description: 'A description of the block functionality.', + table: { + type: { summary: 'string' }, + }, + }, + icon: { + control: 'select', + options: [ 'paragraph', 'cog', 'box', 'button' ], + mapping: { + paragraph, + cog, + box, + button, + }, + description: + 'The icon of the block. This can be any of [WordPress Dashicons](https://developer.wordpress.org/resource/dashicons/), or a custom `svg` element.', + table: { + type: { summary: 'string | object' }, + }, + }, + name: { + control: 'text', + description: 'Optional custom name for the block.', + table: { + type: { summary: 'string' }, + }, + }, + className: { + control: 'text', + description: 'Additional CSS class names.', + table: { + type: { summary: 'string' }, + }, + }, + }, +}; + +export default meta; + +export const Default = { + args: { + title: 'Paragraph', + icon: paragraph, + description: 'This is a paragraph block description.', + name: 'Paragraph Block', + }, +}; diff --git a/packages/block-editor/src/components/block-card/style.scss b/packages/block-editor/src/components/block-card/style.scss index 42cf77aa4b0a84..a5cb675597908b 100644 --- a/packages/block-editor/src/components/block-card/style.scss +++ b/packages/block-editor/src/components/block-card/style.scss @@ -7,15 +7,22 @@ .block-editor-block-card__title { font-weight: 500; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: calc($grid-unit-10 / 2) $grid-unit-10; &.block-editor-block-card__title { font-size: $default-font-size; line-height: $default-line-height; margin: 0; - padding: 3px 0; // This makes the title as high as the icon. } } +.block-editor-block-card__name { + padding: 3px 0; // This makes the title as high as the icon. +} + .block-editor-block-card .block-editor-block-icon { flex: 0 0 $button-size-small; margin-left: 0; @@ -27,3 +34,4 @@ .block-editor-block-card.is-synced .block-editor-block-icon { color: var(--wp-block-synced-color); } + diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 83d0e3f406f829..27d3650f3a0902 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -6,18 +6,27 @@ import clsx from 'clsx'; /** * WordPress dependencies */ -import { withFilters } from '@wordpress/components'; import { getBlockDefaultClassName, - hasBlockSupport, getBlockType, + hasBlockSupport, + store as blocksStore, } from '@wordpress/blocks'; -import { useContext, useMemo } from '@wordpress/element'; +import { withFilters } from '@wordpress/components'; +import { useRegistry, useSelect } from '@wordpress/data'; +import { useCallback, useContext, useMemo } from '@wordpress/element'; /** * Internal dependencies */ import BlockContext from '../block-context'; +import isURLLike from '../link-control/is-url-like'; +import { + canBindAttribute, + hasPatternOverridesDefaultBinding, + replacePatternOverridesDefaultBinding, +} from '../../utils/block-bindings'; +import { unlock } from '../../lock-unlock'; /** * Default value used for blocks which do not define their own context needs, @@ -48,27 +57,223 @@ const Edit = ( props ) => { const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); const EditWithGeneratedProps = ( props ) => { - const { attributes = {}, name } = props; + const { name, clientId, attributes, setAttributes } = props; + const registry = useRegistry(); const blockType = getBlockType( name ); const blockContext = useContext( BlockContext ); + const registeredSources = useSelect( + ( select ) => + unlock( select( blocksStore ) ).getAllBlockBindingsSources(), + [] + ); - // Assign context values using the block type's declared context needs. - const context = useMemo( () => { - return blockType && blockType.usesContext + const { blockBindings, context, hasPatternOverrides } = useMemo( () => { + // Assign context values using the block type's declared context needs. + const computedContext = blockType?.usesContext ? Object.fromEntries( Object.entries( blockContext ).filter( ( [ key ] ) => blockType.usesContext.includes( key ) ) ) : DEFAULT_BLOCK_CONTEXT; - }, [ blockType, blockContext ] ); + // Add context requested by Block Bindings sources. + if ( attributes?.metadata?.bindings ) { + Object.values( attributes?.metadata?.bindings || {} ).forEach( + ( binding ) => { + registeredSources[ binding?.source ]?.usesContext?.forEach( + ( key ) => { + computedContext[ key ] = blockContext[ key ]; + } + ); + } + ); + } + return { + blockBindings: replacePatternOverridesDefaultBinding( + name, + attributes?.metadata?.bindings + ), + context: computedContext, + hasPatternOverrides: hasPatternOverridesDefaultBinding( + attributes?.metadata?.bindings + ), + }; + }, [ + name, + blockType?.usesContext, + blockContext, + attributes?.metadata?.bindings, + registeredSources, + ] ); + + const computedAttributes = useSelect( + ( select ) => { + if ( ! blockBindings ) { + return attributes; + } + + const attributesFromSources = {}; + const blockBindingsBySource = new Map(); + + for ( const [ attributeName, binding ] of Object.entries( + blockBindings + ) ) { + const { source: sourceName, args: sourceArgs } = binding; + const source = registeredSources[ sourceName ]; + if ( ! source || ! canBindAttribute( name, attributeName ) ) { + continue; + } + + blockBindingsBySource.set( source, { + ...blockBindingsBySource.get( source ), + [ attributeName ]: { + args: sourceArgs, + }, + } ); + } + + if ( blockBindingsBySource.size ) { + for ( const [ source, bindings ] of blockBindingsBySource ) { + // Get values in batch if the source supports it. + let values = {}; + if ( ! source.getValues ) { + Object.keys( bindings ).forEach( ( attr ) => { + // Default to the the source label when `getValues` doesn't exist. + values[ attr ] = source.label; + } ); + } else { + values = source.getValues( { + select, + context, + clientId, + bindings, + } ); + } + for ( const [ attributeName, value ] of Object.entries( + values + ) ) { + if ( + attributeName === 'url' && + ( ! value || ! isURLLike( value ) ) + ) { + // Return null if value is not a valid URL. + attributesFromSources[ attributeName ] = null; + } else { + attributesFromSources[ attributeName ] = value; + } + } + } + } + + return { + ...attributes, + ...attributesFromSources, + }; + }, + [ + attributes, + blockBindings, + clientId, + context, + name, + registeredSources, + ] + ); + + const setBoundAttributes = useCallback( + ( nextAttributes ) => { + if ( ! blockBindings ) { + setAttributes( nextAttributes ); + return; + } + + registry.batch( () => { + const keptAttributes = { ...nextAttributes }; + const blockBindingsBySource = new Map(); + + // Loop only over the updated attributes to avoid modifying the bound ones that haven't changed. + for ( const [ attributeName, newValue ] of Object.entries( + keptAttributes + ) ) { + if ( + ! blockBindings[ attributeName ] || + ! canBindAttribute( name, attributeName ) + ) { + continue; + } + + const binding = blockBindings[ attributeName ]; + const source = registeredSources[ binding?.source ]; + if ( ! source?.setValues ) { + continue; + } + blockBindingsBySource.set( source, { + ...blockBindingsBySource.get( source ), + [ attributeName ]: { + args: binding.args, + newValue, + }, + } ); + delete keptAttributes[ attributeName ]; + } + + if ( blockBindingsBySource.size ) { + for ( const [ + source, + bindings, + ] of blockBindingsBySource ) { + source.setValues( { + select: registry.select, + dispatch: registry.dispatch, + context, + clientId, + bindings, + } ); + } + } + + const hasParentPattern = !! context[ 'pattern/overrides' ]; + + if ( + // Don't update non-connected attributes if the block is using pattern overrides + // and the editing is happening while overriding the pattern (not editing the original). + ! ( hasPatternOverrides && hasParentPattern ) && + Object.keys( keptAttributes ).length + ) { + // Don't update caption and href until they are supported. + if ( hasPatternOverrides ) { + delete keptAttributes.caption; + delete keptAttributes.href; + } + setAttributes( keptAttributes ); + } + } ); + }, + [ + blockBindings, + clientId, + context, + hasPatternOverrides, + setAttributes, + registeredSources, + name, + registry, + ] + ); if ( ! blockType ) { return null; } if ( blockType.apiVersion > 1 ) { - return <EditWithFilters { ...props } context={ context } />; + return ( + <EditWithFilters + { ...props } + attributes={ computedAttributes } + context={ context } + setAttributes={ setBoundAttributes } + /> + ); } // Generate a class name for the block's editable form. @@ -77,15 +282,17 @@ const EditWithGeneratedProps = ( props ) => { : null; const className = clsx( generatedClassName, - attributes.className, + attributes?.className, props.className ); return ( <EditWithFilters { ...props } - context={ context } + attributes={ computedAttributes } className={ className } + context={ context } + setAttributes={ setBoundAttributes } /> ); }; diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 2d91108ccb4123..bcf6783a10d1c3 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -12,11 +12,7 @@ import { useDispatch, useRegistry, } from '@wordpress/data'; -import { - useViewportMatch, - useMergeRefs, - useDebounce, -} from '@wordpress/compose'; +import { useMergeRefs, useDebounce } from '@wordpress/compose'; import { createContext, useMemo, @@ -46,7 +42,6 @@ export const IntersectionObserver = createContext(); const pendingBlockVisibilityUpdatesPerRegistry = new WeakMap(); function Root( { className, ...settings } ) { - const isLargeViewport = useViewportMatch( 'medium' ); const { isOutlineMode, isFocusMode, temporarilyEditingAsBlocks } = useSelect( ( select ) => { const { getSettings, getTemporarilyEditingAsBlocks, isTyping } = @@ -105,7 +100,7 @@ function Root( { className, ...settings } ) { ] ), className: clsx( 'is-root-container', className, { 'is-outline-mode': isOutlineMode, - 'is-focus-mode': isFocusMode && isLargeViewport, + 'is-focus-mode': isFocusMode, } ), }, settings diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 554adccdc22c9d..14cda82fe7cd26 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -29,7 +29,7 @@ import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; import { useScrollIntoView } from './use-scroll-into-view'; import { useFlashEditableBlocks } from '../../use-flash-editable-blocks'; -import { canBindBlock } from '../../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../../utils/block-bindings'; import { useFirefoxDraggableCompatibility } from './use-firefox-draggable-compatibility'; /** diff --git a/packages/block-editor/src/components/block-list/zoom-out-separator.js b/packages/block-editor/src/components/block-list/zoom-out-separator.js index f2e6d050141fb5..86191c1e4ce32c 100644 --- a/packages/block-editor/src/components/block-list/zoom-out-separator.js +++ b/packages/block-editor/src/components/block-list/zoom-out-separator.js @@ -33,6 +33,7 @@ export function ZoomOutSeparator( { insertionPoint, blockInsertionPointVisible, blockInsertionPoint, + blocksBeingDragged, } = useSelect( ( select ) => { const { getInsertionPoint, @@ -40,6 +41,7 @@ export function ZoomOutSeparator( { getSectionRootClientId, isBlockInsertionPointVisible, getBlockInsertionPoint, + getDraggedBlockClientIds, } = unlock( select( blockEditorStore ) ); const root = getSectionRootClientId(); @@ -51,6 +53,7 @@ export function ZoomOutSeparator( { insertionPoint: getInsertionPoint(), blockInsertionPoint: getBlockInsertionPoint(), blockInsertionPointVisible: isBlockInsertionPointVisible(), + blocksBeingDragged: getDraggedBlockClientIds(), }; }, [] ); @@ -78,6 +81,7 @@ export function ZoomOutSeparator( { insertionPoint && insertionPoint.hasOwnProperty( 'index' ) && clientId === sectionClientIds[ insertionPoint.index - 1 ]; + // We want to show the zoom out separator in either of these conditions: // 1. If the inserter has an insertion index set // 2. We are dragging a pattern over an insertion point @@ -97,6 +101,32 @@ export function ZoomOutSeparator( { sectionClientIds[ blockInsertionPoint.index - 1 ] ); } + const blockBeingDraggedClientId = blocksBeingDragged[ 0 ]; + + const isCurrentBlockBeingDragged = blocksBeingDragged.includes( clientId ); + + const blockBeingDraggedIndex = sectionClientIds.indexOf( + blockBeingDraggedClientId + ); + const blockBeingDraggedPreviousSiblingClientId = + blockBeingDraggedIndex > 0 + ? sectionClientIds[ blockBeingDraggedIndex - 1 ] + : null; + + const isCurrentBlockPreviousSiblingOfBlockBeingDragged = + blockBeingDraggedPreviousSiblingClientId === clientId; + + // The separators are visually top/bottom of the block, but in actual fact + // the "top" separator is the "bottom" separator of the previous block. + // Therefore, this logic hides the separator if the current block is being dragged + // or if the current block is the previous sibling of the block being dragged. + if ( + isCurrentBlockBeingDragged || + isCurrentBlockPreviousSiblingOfBlockBeingDragged + ) { + isVisible = false; + } + return ( <AnimatePresence> { isVisible && ( diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index b9caee7c338beb..ade9ddd5ec1657 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -65,7 +65,6 @@ export function BlockSettingsDropdown( { selectedBlockClientIds, openedBlockSettingsMenu, isContentOnly, - isZoomOut, } = useSelect( ( select ) => { const { @@ -76,7 +75,6 @@ export function BlockSettingsDropdown( { getBlockAttributes, getOpenedBlockSettingsMenu, getBlockEditingMode, - isZoomOut: _isZoomOut, } = unlock( select( blockEditorStore ) ); const { getActiveBlockVariation } = select( blocksStore ); @@ -101,7 +99,6 @@ export function BlockSettingsDropdown( { openedBlockSettingsMenu: getOpenedBlockSettingsMenu(), isContentOnly: getBlockEditingMode( firstBlockClientId ) === 'contentOnly', - isZoomOut: _isZoomOut(), }; }, [ firstBlockClientId ] @@ -253,15 +250,13 @@ export function BlockSettingsDropdown( { clientId={ firstBlockClientId } /> ) } - { ( ! isContentOnly || isZoomOut ) && ( - <CopyMenuItem - clientIds={ clientIds } - onCopy={ onCopy } - shortcut={ displayShortcut.primary( - 'c' - ) } - /> - ) } + <CopyMenuItem + clientIds={ clientIds } + onCopy={ onCopy } + shortcut={ displayShortcut.primary( + 'c' + ) } + /> { canDuplicate && ( <MenuItem onClick={ pipe( @@ -316,14 +311,16 @@ export function BlockSettingsDropdown( { </MenuItem> </MenuGroup> ) } - <BlockSettingsMenuControls.Slot - fillProps={ { - onClose, - count, - firstBlockClientId, - } } - clientIds={ clientIds } - /> + { ! isContentOnly && ( + <BlockSettingsMenuControls.Slot + fillProps={ { + onClose, + count, + firstBlockClientId, + } } + clientIds={ clientIds } + /> + ) } { typeof children === 'function' ? children( { onClose } ) : Children.map( ( child ) => diff --git a/packages/block-editor/src/components/block-styles/utils.js b/packages/block-editor/src/components/block-styles/utils.js index 511e78da83da60..e4483ec4e695f8 100644 --- a/packages/block-editor/src/components/block-styles/utils.js +++ b/packages/block-editor/src/components/block-styles/utils.js @@ -10,7 +10,7 @@ import { _x } from '@wordpress/i18n'; * @param {Array} styles Block styles. * @param {string} className Class name * - * @return {Object?} The active style. + * @return {?Object} The active style. */ export function getActiveStyle( styles, className ) { for ( const style of new TokenList( className ).values() ) { @@ -34,7 +34,7 @@ export function getActiveStyle( styles, className ) { * Replaces the active style in the block's className. * * @param {string} className Class name. - * @param {Object?} activeStyle The replaced style. + * @param {?Object} activeStyle The replaced style. * @param {Object} newStyle The replacing style. * * @return {string} The updated className. @@ -83,7 +83,7 @@ export function getRenderedStyles( styles ) { * * @param {Array} styles Block styles. * - * @return {Object?} The default style object, if found. + * @return {?Object} The default style object, if found. */ export function getDefaultStyle( styles ) { return styles?.find( ( style ) => style.isDefault ); diff --git a/packages/block-editor/src/components/block-title/stories/index.story.js b/packages/block-editor/src/components/block-title/stories/index.story.js new file mode 100644 index 00000000000000..dc66fc721e5158 --- /dev/null +++ b/packages/block-editor/src/components/block-title/stories/index.story.js @@ -0,0 +1,76 @@ +/** + * WordPress dependencies + */ +import { registerCoreBlocks } from '@wordpress/block-library'; +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { ExperimentalBlockEditorProvider } from '../../provider'; +import BlockTitle from '../'; + +// Register core blocks for the story environment +registerCoreBlocks(); + +// Sample blocks for testing +const blocks = [ createBlock( 'core/paragraph' ) ]; + +const meta = { + title: 'BlockEditor/BlockTitle', + component: BlockTitle, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: + "Renders the block's configured title as a string, or empty if the title cannot be determined.", + }, + }, + }, + decorators: [ + ( Story ) => ( + <ExperimentalBlockEditorProvider value={ blocks }> + <Story /> + </ExperimentalBlockEditorProvider> + ), + ], + argTypes: { + clientId: { + control: { type: null }, + description: 'Client ID of block.', + table: { + type: { + summary: 'string', + }, + }, + }, + maximumLength: { + control: { type: 'number' }, + description: + 'The maximum length that the block title string may be before truncated.', + table: { + type: { + summary: 'number', + }, + }, + }, + context: { + control: { type: 'text' }, + description: 'The context to pass to `getBlockLabel`.', + table: { + type: { + summary: 'string', + }, + }, + }, + }, +}; + +export default meta; + +export const Default = { + args: { + clientId: blocks[ 0 ].clientId, + }, +}; diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 80fe4c420d1e1f..35d075c1a99b78 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -78,6 +78,7 @@ color: $white; padding: 0; + // TODO: Consider passing size="small" to the Inserter toggle instead. // Special dimensions for this button. min-width: $button-size-small; height: $button-size-small; diff --git a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js index 17af902bf9baf2..56b8d46b067844 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js @@ -20,6 +20,8 @@ function ZoomOutModeInserters() { setInserterIsOpened, sectionRootClientId, selectedBlockClientId, + blockInsertionPoint, + insertionPointVisible, } = useSelect( ( select ) => { const { getSettings, @@ -27,6 +29,8 @@ function ZoomOutModeInserters() { getSelectionStart, getSelectedBlockClientId, getSectionRootClientId, + getBlockInsertionPoint, + isBlockInsertionPointVisible, } = unlock( select( blockEditorStore ) ); const root = getSectionRootClientId(); @@ -38,6 +42,8 @@ function ZoomOutModeInserters() { setInserterIsOpened: getSettings().__experimentalSetIsInserterOpened, selectedBlockClientId: getSelectedBlockClientId(), + blockInsertionPoint: getBlockInsertionPoint(), + insertionPointVisible: isBlockInsertionPointVisible(), }; }, [] ); @@ -62,7 +68,19 @@ function ZoomOutModeInserters() { const index = blockOrder.findIndex( ( clientId ) => selectedBlockClientId === clientId ); - const nextClientId = blockOrder[ index + 1 ]; + + const insertionIndex = index + 1; + + const nextClientId = blockOrder[ insertionIndex ]; + + // If the block insertion point is visible, and the insertion + // Indices match then we don't need to render the inserter. + if ( + insertionPointVisible && + blockInsertionPoint?.index === insertionIndex + ) { + return null; + } return ( <BlockPopoverInbetween @@ -73,11 +91,11 @@ function ZoomOutModeInserters() { onClick={ () => { setInserterIsOpened( { rootClientId: sectionRootClientId, - insertionIndex: index + 1, + insertionIndex, tab: 'patterns', category: 'all', } ); - showInsertionPoint( sectionRootClientId, index + 1, { + showInsertionPoint( sectionRootClientId, insertionIndex, { operation: 'insert', } ); } } diff --git a/packages/block-editor/src/components/border-radius-control/README.md b/packages/block-editor/src/components/border-radius-control/README.md new file mode 100644 index 00000000000000..7b048dfdb7e0d2 --- /dev/null +++ b/packages/block-editor/src/components/border-radius-control/README.md @@ -0,0 +1,59 @@ +# BorderRadiusControl + +`BorderRadiusControl` is a React component that provides a user interface for managing border radius values. It allows users to control the border radius of each corner independently or link them together for uniform values. + +## Usage + +```jsx +/** + * WordPress dependencies + */ +import { __experimentalBorderRadiusControl as BorderRadiusControl } from '@wordpress/block-editor'; +import { useState } from '@wordpress/element'; + +const MyBorderRadiusControl = () => { + const [values, setValues] = useState({ + topLeft: '10px', + topRight: '10px', + bottomLeft: '10px', + bottomRight: '10px', + }); + + return ( + <BorderRadiusControl + values={values} + onChange={setValues} + /> + ); +}; +``` + +## Props + +### values + +An object containing the border radius values for each corner. + +- **Type:** `Object` +- **Required:** No +- **Default:** `undefined` + +The values object has the following schema: + +| Property | Description | Type | +| ----------- | ------------------------------------ | ------ | +| topLeft | Border radius for top left corner | string | +| topRight | Border radius for top right corner | string | +| bottomLeft | Border radius for bottom left corner | string | +| bottomRight | Border radius for bottom right corner| string | + +Each value should be a valid CSS border radius value (e.g., '10px', '1em'). + +### onChange + +Callback function that is called when any border radius value changes. + +- **Type:** `Function` +- **Required:** Yes + +The function receives the updated values object as its argument. \ No newline at end of file diff --git a/packages/block-editor/src/components/border-radius-control/stories/index.story.js b/packages/block-editor/src/components/border-radius-control/stories/index.story.js new file mode 100644 index 00000000000000..28844a5e5cace1 --- /dev/null +++ b/packages/block-editor/src/components/border-radius-control/stories/index.story.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import BorderRadiusControl from '../'; + +const meta = { + title: 'BlockEditor/BorderRadiusControl', + component: BorderRadiusControl, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: 'Control to display border radius options.', + }, + }, + }, + argTypes: { + values: { + control: 'object', + description: 'Border radius values.', + table: { + type: { summary: 'object' }, + }, + }, + onChange: { + action: 'onChange', + control: { type: null }, + table: { + type: { summary: 'function' }, + }, + description: 'Callback to handle onChange.', + }, + }, +}; + +export default meta; + +export const Default = { + render: function Template( { onChange, ...args } ) { + const [ values, setValues ] = useState( args.values ); + + return ( + <BorderRadiusControl + { ...args } + values={ values } + onChange={ ( ...changeArgs ) => { + setValues( ...changeArgs ); + onChange( ...changeArgs ); + } } + /> + ); + }, +}; diff --git a/packages/block-editor/src/components/button-block-appender/index.js b/packages/block-editor/src/components/button-block-appender/index.js index 53b15e2fd2cfdd..4cde8c26d75638 100644 --- a/packages/block-editor/src/components/button-block-appender/index.js +++ b/packages/block-editor/src/components/button-block-appender/index.js @@ -7,7 +7,7 @@ import clsx from 'clsx'; * WordPress dependencies */ import { Button } from '@wordpress/components'; -import { forwardRef, useRef } from '@wordpress/element'; +import { forwardRef } from '@wordpress/element'; import { _x, sprintf } from '@wordpress/i18n'; import { Icon, plus } from '@wordpress/icons'; import deprecated from '@wordpress/deprecated'; @@ -16,15 +16,11 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import Inserter from '../inserter'; -import { useMergeRefs } from '@wordpress/compose'; function ButtonBlockAppender( { rootClientId, className, onFocus, tabIndex, onSelect }, ref ) { - const inserterButtonRef = useRef(); - - const mergedInserterButtonRef = useMergeRefs( [ inserterButtonRef, ref ] ); return ( <Inserter position="bottom center" @@ -34,7 +30,6 @@ function ButtonBlockAppender( if ( onSelect && typeof onSelect === 'function' ) { onSelect( ...args ); } - inserterButtonRef.current?.focus(); } } renderToggle={ ( { onToggle, @@ -61,7 +56,7 @@ function ButtonBlockAppender( return ( <Button __next40pxDefaultSize - ref={ mergedInserterButtonRef } + ref={ ref } onFocus={ onFocus } tabIndex={ tabIndex } className={ clsx( diff --git a/packages/block-editor/src/components/child-layout-control/index.js b/packages/block-editor/src/components/child-layout-control/index.js index 022acf2e1074a4..20791d9751bcd4 100644 --- a/packages/block-editor/src/components/child-layout-control/index.js +++ b/packages/block-editor/src/components/child-layout-control/index.js @@ -9,6 +9,7 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, __experimentalToolsPanelItem as ToolsPanelItem, + __experimentalUseCustomUnits as useCustomUnits, Flex, FlexItem, } from '@wordpress/components'; @@ -21,6 +22,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; */ import { useGetNumberOfBlocksBeforeCell } from '../grid/use-get-number-of-blocks-before-cell'; import { store as blockEditorStore } from '../../store'; +import { useSettings } from '../use-settings'; function helpText( selfStretch, parentLayout ) { const { orientation = 'horizontal' } = parentLayout; @@ -98,6 +100,17 @@ function FlexControls( { const hasFlexValue = () => !! selfStretch; const flexResetLabel = orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' ); + const [ availableUnits ] = useSettings( 'spacing.units' ); + const units = useCustomUnits( { + availableUnits: availableUnits || [ + '%', + 'px', + 'em', + 'rem', + 'vh', + 'vw', + ], + } ); const resetFlex = () => { onChange( { selfStretch: undefined, @@ -167,6 +180,7 @@ function FlexControls( { { selfStretch === 'fixed' && ( <UnitControl size="__unstable-large" + units={ units } onChange={ ( value ) => { onChange( { selfStretch, diff --git a/packages/block-editor/src/components/colors-gradients/dropdown.js b/packages/block-editor/src/components/colors-gradients/dropdown.js index 71b27c06e7ccfc..e667927bee7601 100644 --- a/packages/block-editor/src/components/colors-gradients/dropdown.js +++ b/packages/block-editor/src/components/colors-gradients/dropdown.js @@ -15,6 +15,13 @@ import { __experimentalHStack as HStack, __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; +import { useRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { reset as resetIcon } from '@wordpress/icons'; /** * Internal dependencies @@ -76,7 +83,15 @@ const LabeledColorIndicator = ( { colorValue, label } ) => ( const renderToggle = ( settings ) => ( { onToggle, isOpen } ) => { - const { colorValue, label } = settings; + const { + clearable, + colorValue, + gradientValue, + onColorChange, + onGradientChange, + label, + } = settings; + const colorButtonRef = useRef( undefined ); const toggleProps = { onClick: onToggle, @@ -85,15 +100,45 @@ const renderToggle = { 'is-open': isOpen } ), 'aria-expanded': isOpen, + ref: colorButtonRef, + }; + + const clearValue = () => { + if ( colorValue ) { + onColorChange(); + } else if ( gradientValue ) { + onGradientChange(); + } }; + const value = colorValue ?? gradientValue; + return ( - <Button __next40pxDefaultSize { ...toggleProps }> - <LabeledColorIndicator - colorValue={ colorValue } - label={ label } - /> - </Button> + <> + <Button __next40pxDefaultSize { ...toggleProps }> + <LabeledColorIndicator + colorValue={ value } + label={ label } + /> + </Button> + { clearable && value && ( + <Button + __next40pxDefaultSize + label={ __( 'Reset' ) } + className="block-editor-panel-color-gradient-settings__reset" + size="small" + icon={ resetIcon } + onClick={ () => { + clearValue(); + if ( isOpen ) { + onToggle(); + } + // Return focus to parent button + colorButtonRef.current?.focus(); + } } + /> + ) } + </> ); }; @@ -143,8 +188,12 @@ export default function ColorGradientSettingsDropdown( { ...setting, }; const toggleSettings = { - colorValue: setting.gradientValue ?? setting.colorValue, + clearable: setting.clearable, label: setting.label, + colorValue: setting.colorValue, + gradientValue: setting.gradientValue, + onColorChange: setting.onColorChange, + onGradientChange: setting.onGradientChange, }; return ( diff --git a/packages/block-editor/src/components/colors-gradients/style.scss b/packages/block-editor/src/components/colors-gradients/style.scss index 222a5b239cf992..fbdf144a4176b2 100644 --- a/packages/block-editor/src/components/colors-gradients/style.scss +++ b/packages/block-editor/src/components/colors-gradients/style.scss @@ -140,4 +140,9 @@ $swatch-gap: 12px; &:hover { opacity: 1; } + + @media (hover: none) { + // Show reset button on devices that do not support hover. + opacity: 1; + } } diff --git a/packages/block-editor/src/components/contrast-checker/index.native.js b/packages/block-editor/src/components/contrast-checker/index.native.js index edd60473fcc36e..c4f19857ccec7e 100644 --- a/packages/block-editor/src/components/contrast-checker/index.native.js +++ b/packages/block-editor/src/components/contrast-checker/index.native.js @@ -13,7 +13,7 @@ import { speak } from '@wordpress/a11y'; import { __ } from '@wordpress/i18n'; import { useEffect } from '@wordpress/element'; import { usePreferredColorSchemeStyle } from '@wordpress/compose'; -import { Icon, warning } from '@wordpress/icons'; +import { Icon, cautionFilled } from '@wordpress/icons'; /** * Internal dependencies */ @@ -52,7 +52,7 @@ function ContrastCheckerMessage( { return ( <View style={ styles[ 'block-editor-contrast-checker' ] }> - <Icon style={ iconStyle } icon={ warning } /> + <Icon style={ iconStyle } icon={ cautionFilled } /> <Text style={ msgStyle }>{ msg }</Text> </View> ); diff --git a/packages/block-editor/src/components/contrast-checker/stories/index.story.js b/packages/block-editor/src/components/contrast-checker/stories/index.story.js new file mode 100644 index 00000000000000..4518ab2ba7cd67 --- /dev/null +++ b/packages/block-editor/src/components/contrast-checker/stories/index.story.js @@ -0,0 +1,117 @@ +/** + * Internal dependencies + */ +import ContrastChecker from '../'; + +const meta = { + title: 'BlockEditor/ContrastChecker', + component: ContrastChecker, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: + 'Determines if contrast for text styles is sufficient (WCAG 2.0 AA) when used with a given background color.', + }, + }, + }, + argTypes: { + backgroundColor: { + control: 'color', + description: + 'The background color to check the contrast of text against.', + table: { + type: { + summary: 'string', + }, + }, + }, + fallbackBackgroundColor: { + control: 'color', + description: + 'A fallback background color value, in case `backgroundColor` is not available.', + table: { + type: { + summary: 'string', + }, + }, + }, + textColor: { + control: 'color', + description: + 'The text color to check the contrast of the background against.', + table: { + type: { + summary: 'string', + }, + }, + }, + fallbackTextColor: { + control: 'color', + description: + 'A fallback text color value, in case `textColor` is not available.', + table: { + type: { + summary: 'string', + }, + }, + }, + fontSize: { + control: 'number', + description: + 'The font-size (as a `px` value) of the text to check the contrast against.', + table: { + type: { + summary: 'number', + }, + }, + }, + isLargeText: { + control: 'boolean', + description: + 'Whether the text is large (approximately `24px` or higher).', + table: { + type: { + summary: 'boolean', + }, + }, + }, + linkColor: { + control: 'color', + description: 'The link color to check the contrast against.', + table: { + type: { + summary: 'string', + }, + }, + }, + fallbackLinkColor: { + control: 'color', + description: 'Fallback link color if linkColor is not available.', + table: { + type: { + summary: 'string', + }, + }, + }, + enableAlphaChecker: { + control: 'boolean', + description: 'Whether to enable checking for transparent colors.', + table: { + type: { + summary: 'boolean', + }, + defaultValue: { summary: false }, + }, + }, + }, +}; + +export default meta; + +export const Default = { + args: { + backgroundColor: '#ffffff', + textColor: '#ffffff', + }, +}; diff --git a/packages/block-editor/src/components/date-format-picker/README.md b/packages/block-editor/src/components/date-format-picker/README.md index e057bdc31a1680..f6160cb90955b5 100644 --- a/packages/block-editor/src/components/date-format-picker/README.md +++ b/packages/block-editor/src/components/date-format-picker/README.md @@ -1,17 +1,12 @@ # DateFormatPicker -The `DateFormatPicker` component renders controls that let the user choose a -_date format_. That is, how they want their dates to be formatted. +The `DateFormatPicker` component renders controls that let the user choose a _date format_. That is, how they want their dates to be formatted. -A user can pick _Default_ to use the default date format (usually set at the -site level). +A user can pick _Default_ to use the default date format (usually set at the site level). -Otherwise, a user may choose a suggested date format or type in their own date -format by selecting _Custom_. +Otherwise, a user may choose a suggested date format or type in their own date format by selecting _Custom_. -All date format strings should be in the format accepted by by the [`dateI18n` -function in -`@wordpress/date`](https://github.com/WordPress/gutenberg/tree/trunk/packages/date#datei18n). +All date format strings should be in the format accepted by by the [`dateI18n` function in `@wordpress/date`](https://github.com/WordPress/gutenberg/tree/trunk/packages/date#datei18n). ## Usage @@ -43,16 +38,14 @@ The current date format selected by the user. If `null`, _Default_ is selected. ### `defaultFormat` -The default format string. Used to show to the user what the date will look like -if _Default_ is selected. +The default format string. Used to show to the user what the date will look like if _Default_ is selected. - Type: `string` - Required: Yes ### `onChange` -Called when the user makes a selection, or when the user types in a date format. -`null` indicates that _Default_ is selected. +Called when the user makes a selection, or when the user types in a date format. `null` indicates that _Default_ is selected. - Type: `( format: string|null ) => void` - Required: Yes diff --git a/packages/block-editor/src/components/date-format-picker/index.js b/packages/block-editor/src/components/date-format-picker/index.js index eb269e03ca5abc..6854ee74a0162b 100644 --- a/packages/block-editor/src/components/date-format-picker/index.js +++ b/packages/block-editor/src/components/date-format-picker/index.js @@ -29,21 +29,10 @@ if ( exampleDate.getMonth() === 4 ) { * * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/date-format-picker/README.md * - * @param {Object} props - * @param {string|null} props.format The selected date - * format. If - * `null`, - * _Default_ is - * selected. - * @param {string} props.defaultFormat The date format that - * will be used if the - * user selects - * 'Default'. - * @param {( format: string|null ) => void} props.onChange Called when a - * selection is - * made. If `null`, - * _Default_ is - * selected. + * @param {Object} props + * @param {string|null} props.format The selected date format. If `null`, _Default_ is selected. + * @param {string} props.defaultFormat The date format that will be used if the user selects 'Default'. + * @param {Function} props.onChange Called when a selection is made. If `null`, _Default_ is selected. */ export default function DateFormatPicker( { format, @@ -51,7 +40,11 @@ export default function DateFormatPicker( { onChange, } ) { return ( - <fieldset className="block-editor-date-format-picker"> + <VStack + as="fieldset" + spacing={ 4 } + className="block-editor-date-format-picker" + > <VisuallyHidden as="legend">{ __( 'Date format' ) }</VisuallyHidden> <ToggleControl __nextHasNoMarginBottom @@ -68,7 +61,7 @@ export default function DateFormatPicker( { { format && ( <NonDefaultControls format={ format } onChange={ onChange } /> ) } - </fieldset> + </VStack> ); } diff --git a/packages/block-editor/src/components/date-format-picker/stories/index.story.js b/packages/block-editor/src/components/date-format-picker/stories/index.story.js new file mode 100644 index 00000000000000..12d7e071054949 --- /dev/null +++ b/packages/block-editor/src/components/date-format-picker/stories/index.story.js @@ -0,0 +1,69 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import DateFormatPicker from '../'; + +export default { + title: 'BlockEditor/DateFormatPicker', + component: DateFormatPicker, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: + 'The `DateFormatPicker` component enables users to configure their preferred *date format*. This determines how dates are displayed.', + }, + }, + }, + argTypes: { + defaultFormat: { + control: 'text', + description: + 'The date format that will be used if the user selects "Default".', + table: { + type: { summary: 'string' }, + }, + }, + format: { + control: { type: null }, + description: + 'The selected date format. If `null`, _Default_ is selected.', + table: { + type: { summary: 'string | null' }, + }, + }, + onChange: { + action: 'onChange', + control: { type: null }, + description: + 'Called when a selection is made. If `null`, _Default_ is selected.', + table: { + type: { summary: 'function' }, + }, + }, + }, +}; + +export const Default = { + args: { + defaultFormat: 'M j, Y', + }, + render: function Template( { onChange, ...args } ) { + const [ format, setFormat ] = useState(); + return ( + <DateFormatPicker + { ...args } + onChange={ ( ...changeArgs ) => { + onChange( ...changeArgs ); + setFormat( ...changeArgs ); + } } + format={ format } + /> + ); + }, +}; diff --git a/packages/block-editor/src/components/date-format-picker/style.scss b/packages/block-editor/src/components/date-format-picker/style.scss index 748e43bb8db94a..55f844a9ac887b 100644 --- a/packages/block-editor/src/components/date-format-picker/style.scss +++ b/packages/block-editor/src/components/date-format-picker/style.scss @@ -1,5 +1,7 @@ .block-editor-date-format-picker { - margin-bottom: $grid-unit-20; + margin: 0 0 $grid-unit-20; + padding: 0; + border: none; } .block-editor-date-format-picker__custom-format-select-control__custom-option { diff --git a/packages/block-editor/src/components/default-block-appender/content.scss b/packages/block-editor/src/components/default-block-appender/content.scss index 71ede90d25c0ca..361268fb2b37de 100644 --- a/packages/block-editor/src/components/default-block-appender/content.scss +++ b/packages/block-editor/src/components/default-block-appender/content.scss @@ -42,6 +42,7 @@ color: $white; padding: 0; + // TODO: Consider passing size="small" to the Inserter toggle instead. // Special dimensions for this button. min-width: $button-size-small; height: $button-size-small; diff --git a/packages/block-editor/src/components/dimensions-tool/stories/aspect-ratio-tool.story.js b/packages/block-editor/src/components/dimensions-tool/stories/aspect-ratio-tool.story.js index b853d780052942..aeb8a5f957425f 100644 --- a/packages/block-editor/src/components/dimensions-tool/stories/aspect-ratio-tool.story.js +++ b/packages/block-editor/src/components/dimensions-tool/stories/aspect-ratio-tool.story.js @@ -13,8 +13,9 @@ import { import AspectRatioTool from '../aspect-ratio-tool'; export default { - title: 'BlockEditor (Private APIs)/DimensionsTool/AspectRatioTool', + title: 'BlockEditor/DimensionsTool/AspectRatioTool', component: AspectRatioTool, + tags: [ 'status-private' ], argTypes: { panelId: { control: false }, onChange: { action: 'changed' }, diff --git a/packages/block-editor/src/components/dimensions-tool/stories/index.story.js b/packages/block-editor/src/components/dimensions-tool/stories/index.story.js index ebf08fba0c686b..0ccfba2b9e97a6 100644 --- a/packages/block-editor/src/components/dimensions-tool/stories/index.story.js +++ b/packages/block-editor/src/components/dimensions-tool/stories/index.story.js @@ -13,8 +13,9 @@ import { import DimensionsTool from '..'; export default { - title: 'BlockEditor (Private APIs)/DimensionsTool', + title: 'BlockEditor/DimensionsTool/DimensionsTool', component: DimensionsTool, + tags: [ 'status-private' ], argTypes: { panelId: { control: false }, onChange: { action: 'changed' }, diff --git a/packages/block-editor/src/components/dimensions-tool/stories/scale-tool.story.js b/packages/block-editor/src/components/dimensions-tool/stories/scale-tool.story.js index b485bf68a892d9..ea0a3ec194beed 100644 --- a/packages/block-editor/src/components/dimensions-tool/stories/scale-tool.story.js +++ b/packages/block-editor/src/components/dimensions-tool/stories/scale-tool.story.js @@ -13,8 +13,9 @@ import { import ScaleTool from '../scale-tool'; export default { - title: 'BlockEditor (Private APIs)/DimensionsTool/ScaleTool', + title: 'BlockEditor/DimensionsTool/ScaleTool', component: ScaleTool, + tags: [ 'status-private' ], argTypes: { panelId: { control: false }, onChange: { action: 'changed' }, diff --git a/packages/block-editor/src/components/dimensions-tool/stories/width-height-tool.story.js b/packages/block-editor/src/components/dimensions-tool/stories/width-height-tool.story.js index eed3cbc02f466e..86b3b4b22be60d 100644 --- a/packages/block-editor/src/components/dimensions-tool/stories/width-height-tool.story.js +++ b/packages/block-editor/src/components/dimensions-tool/stories/width-height-tool.story.js @@ -13,8 +13,9 @@ import { import WidthHeightTool from '../width-height-tool'; export default { - title: 'BlockEditor (Private APIs)/DimensionsTool/WidthHeightTool', + title: 'BlockEditor/DimensionsTool/WidthHeightTool', component: WidthHeightTool, + tags: [ 'status-private' ], argTypes: { panelId: { control: false }, onChange: { action: 'changed' }, diff --git a/packages/block-editor/src/components/font-appearance-control/index.js b/packages/block-editor/src/components/font-appearance-control/index.js index f9e8023f93ec69..62396c2dc7bd64 100644 --- a/packages/block-editor/src/components/font-appearance-control/index.js +++ b/packages/block-editor/src/components/font-appearance-control/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { CustomSelectControl } from '@wordpress/components'; +import deprecated from '@wordpress/deprecated'; import { useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -147,6 +148,20 @@ export default function FontAppearanceControl( props ) { ); }; + if ( + ! __next40pxDefaultSize && + ( otherProps.size === undefined || otherProps.size === 'default' ) + ) { + deprecated( + `36px default size for wp.blockEditor.__experimentalFontAppearanceControl`, + { + since: '6.8', + version: '7.1', + hint: 'Set the `__next40pxDefaultSize` prop to true to start opting into the new default size, which will become the default in a future version.', + } + ); + } + return ( hasStylesOrWeights && ( <CustomSelectControl diff --git a/packages/block-editor/src/components/font-family/README.md b/packages/block-editor/src/components/font-family/README.md index 57697f595cc800..25190802e5d0bf 100644 --- a/packages/block-editor/src/components/font-family/README.md +++ b/packages/block-editor/src/components/font-family/README.md @@ -29,6 +29,7 @@ const MyFontFamilyControl = () => { setFontFamily( newFontFamily ); } } __nextHasNoMarginBottom + __next40pxDefaultSize /> ); }; diff --git a/packages/block-editor/src/components/font-family/index.js b/packages/block-editor/src/components/font-family/index.js index e8d0d7ed2dd808..b685e3990287fe 100644 --- a/packages/block-editor/src/components/font-family/index.js +++ b/packages/block-editor/src/components/font-family/index.js @@ -58,12 +58,28 @@ export default function FontFamilyControl( { ); } + if ( + ! __next40pxDefaultSize && + ( props.size === undefined || props.size === 'default' ) + ) { + deprecated( + `36px default size for wp.blockEditor.__experimentalFontFamilyControl`, + { + since: '6.8', + version: '7.1', + hint: 'Set the `__next40pxDefaultSize` prop to true to start opting into the new default size, which will become the default in a future version.', + } + ); + } + + const selectedValue = + options.find( ( option ) => option.key === value ) ?? ''; return ( <CustomSelectControl __next40pxDefaultSize={ __next40pxDefaultSize } __shouldNotWarnDeprecated36pxSize label={ __( 'Font' ) } - value={ value } + value={ selectedValue } onChange={ ( { selectedItem } ) => onChange( selectedItem.key ) } options={ options } className={ clsx( 'block-editor-font-family-control', className, { diff --git a/packages/block-editor/src/components/font-family/stories/index.story.js b/packages/block-editor/src/components/font-family/stories/index.story.js index 54dadeb213f12c..9077c131cbe3bb 100644 --- a/packages/block-editor/src/components/font-family/stories/index.story.js +++ b/packages/block-editor/src/components/font-family/stories/index.story.js @@ -50,5 +50,6 @@ export const Default = { }, ], __nextHasNoMarginBottom: true, + __next40pxDefaultSize: true, }, }; diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index f1a1834967ed92..5d5c02d179307d 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -250,6 +250,9 @@ function ColorPanelDropdown( { icon={ resetIcon } onClick={ () => { resetValue(); + if ( isOpen ) { + onToggle(); + } // Return focus to parent button colorGradientDropdownButtonRef.current?.focus(); } } diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 93e5cc9afdbb3c..5022e8ba591dbb 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -855,7 +855,7 @@ describe( 'global styles renderer', () => { it( 'should return block selectors data with old experimental selectors', () => { const imageSupports = { - border: { + __experimentalBorder: { radius: true, __experimentalSelector: 'img, .crop-area', }, diff --git a/packages/block-editor/src/components/global-styles/typography-utils.js b/packages/block-editor/src/components/global-styles/typography-utils.js index 4b7c90ae4f222c..2f4d2b4424a6fb 100644 --- a/packages/block-editor/src/components/global-styles/typography-utils.js +++ b/packages/block-editor/src/components/global-styles/typography-utils.js @@ -45,7 +45,7 @@ import { getFontStylesAndWeights } from '../../utils/get-font-styles-and-weights * @param {Preset} preset * @param {Object} settings * @param {boolean|TypographySettings} settings.typography.fluid Whether fluid typography is enabled, and, optionally, fluid font size options. - * @param {Object?} settings.typography.layout Layout options. + * @param {?Object} settings.typography.layout Layout options. * * @return {string|*} A font-size value or the value of preset.size. */ diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index fabc65d143d1aa..cd4ad0cea50e0d 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -47,7 +47,7 @@ const ELEMENT_CLASS_NAMES = { // List of block support features that can have their related styles // generated under their own feature level selector rather than the block's. const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = { - border: 'border', + __experimentalBorder: 'border', color: 'color', spacing: 'spacing', typography: 'typography', @@ -624,7 +624,7 @@ function pickStyleKeys( treeToPickFrom ) { // clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it const clonedEntries = pickedEntries.map( ( [ key, style ] ) => [ key, - structuredClone( style ), + JSON.parse( JSON.stringify( style ) ), ] ); return Object.fromEntries( clonedEntries ); } diff --git a/packages/block-editor/src/components/grid/grid-visualizer.js b/packages/block-editor/src/components/grid/grid-visualizer.js index 81da0457ffc5ca..9d89866bbff5f7 100644 --- a/packages/block-editor/src/components/grid/grid-visualizer.js +++ b/packages/block-editor/src/components/grid/grid-visualizer.js @@ -62,6 +62,17 @@ const GridVisualizerGrid = forwardRef( observer.observe( element ); observers.push( observer ); } + + const mutationObserver = new window.MutationObserver( () => { + setGridInfo( getGridInfo( gridElement ) ); + } ); + mutationObserver.observe( gridElement, { + attributeFilter: [ 'style', 'class' ], + childList: true, + subtree: true, + } ); + observers.push( mutationObserver ); + return () => { for ( const observer of observers ) { observer.disconnect(); diff --git a/packages/block-editor/src/components/grid/utils.js b/packages/block-editor/src/components/grid/utils.js index fc012c645f0916..21014108085423 100644 --- a/packages/block-editor/src/components/grid/utils.js +++ b/packages/block-editor/src/components/grid/utils.js @@ -160,6 +160,21 @@ export function getGridInfo( gridElement ) { gridElement, 'grid-template-rows' ); + const borderTopWidth = getComputedCSS( gridElement, 'border-top-width' ); + const borderRightWidth = getComputedCSS( + gridElement, + 'border-right-width' + ); + const borderBottomWidth = getComputedCSS( + gridElement, + 'border-bottom-width' + ); + const borderLeftWidth = getComputedCSS( gridElement, 'border-left-width' ); + const paddingTop = getComputedCSS( gridElement, 'padding-top' ); + const paddingRight = getComputedCSS( gridElement, 'padding-right' ); + const paddingBottom = getComputedCSS( gridElement, 'padding-bottom' ); + const paddingLeft = getComputedCSS( gridElement, 'padding-left' ); + const numColumns = gridTemplateColumns.split( ' ' ).length; const numRows = gridTemplateRows.split( ' ' ).length; const numItems = numColumns * numRows; @@ -172,7 +187,10 @@ export function getGridInfo( gridElement ) { gridTemplateColumns, gridTemplateRows, gap: getComputedCSS( gridElement, 'gap' ), - padding: getComputedCSS( gridElement, 'padding' ), + paddingTop: `calc(${ paddingTop } + ${ borderTopWidth })`, + paddingRight: `calc(${ paddingRight } + ${ borderRightWidth })`, + paddingBottom: `calc(${ paddingBottom } + ${ borderBottomWidth })`, + paddingLeft: `calc(${ paddingLeft } + ${ borderLeftWidth })`, }, }; } diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 751e940dd166cc..8ec4b24106ebf3 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -192,7 +192,7 @@ function Iframe( { // Appending a hash to the current URL will not reload the // page. This is useful for e.g. footnotes. const href = event.target.getAttribute( 'href' ); - if ( href.startsWith( '#' ) ) { + if ( href?.startsWith( '#' ) ) { iFrameDocument.defaultView.location.hash = href.slice( 1 ); } diff --git a/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js b/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js index fd801779372aac..505785c87914d7 100644 --- a/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js +++ b/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js @@ -7,7 +7,7 @@ import fastDeepEqual from 'fast-deep-equal/es6'; * WordPress dependencies */ import { useRef, useLayoutEffect } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useRegistry } from '@wordpress/data'; import { synchronizeBlocksWithTemplate } from '@wordpress/blocks'; /** @@ -42,14 +42,7 @@ export default function useInnerBlockTemplateSync( ) { // Instead of adding a useSelect mapping here, please add to the useSelect // mapping in InnerBlocks! Every subscription impacts performance. - - const { - getBlocks, - getSelectedBlocksInitialCaretPosition, - isBlockSelected, - } = useSelect( blockEditorStore ); - const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent } = - useDispatch( blockEditorStore ); + const registry = useRegistry(); // Maintain a reference to the previous value so we can do a deep equality check. const existingTemplateRef = useRef( null ); @@ -57,6 +50,14 @@ export default function useInnerBlockTemplateSync( useLayoutEffect( () => { let isCancelled = false; + const { + getBlocks, + getSelectedBlocksInitialCaretPosition, + isBlockSelected, + } = registry.select( blockEditorStore ); + const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent } = + registry.dispatch( blockEditorStore ); + // There's an implicit dependency between useInnerBlockTemplateSync and useNestedSettingsUpdate // The former needs to happen after the latter and since the latter is using microtasks to batch updates (performance optimization), // we need to schedule this one in a microtask as well. @@ -110,5 +111,11 @@ export default function useInnerBlockTemplateSync( return () => { isCancelled = true; }; - }, [ template, templateLock, clientId ] ); + }, [ + template, + templateLock, + clientId, + registry, + templateInsertUpdatesSelection, + ] ); } diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js index 93a03ee200497e..2bc41a7176954c 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/index.js @@ -14,9 +14,8 @@ import { usePatternCategories } from '../block-patterns-tab/use-pattern-categori function PatternsExplorer( { initialCategory, rootClientId } ) { const [ searchValue, setSearchValue ] = useState( '' ); - const [ selectedCategory, setSelectedCategory ] = useState( - initialCategory?.name - ); + const [ selectedCategory, setSelectedCategory ] = + useState( initialCategory ); const patternCategories = usePatternCategories( rootClientId ); diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/index.js b/packages/block-editor/src/components/inserter/block-patterns-tab/index.js index 45db4732aa9c6a..f250ed6f12ebad 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/index.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/index.js @@ -70,7 +70,9 @@ function BlockPatternsTab( { ) } { showPatternsExplorer && ( <PatternsExplorerModal - initialCategory={ selectedCategory || categories[ 0 ] } + initialCategory={ + selectedCategory?.name || categories[ 0 ]?.name + } patternCategories={ categories } onModalClose={ () => setShowPatternsExplorer( false ) } rootClientId={ rootClientId } 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 c6ce9ba97d2501..f9af2b6f8c42d2 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 @@ -69,19 +69,19 @@ export function PatternCategoryPreviews( { return false; } - if ( category.name === allPatternsCategory.name ) { + if ( category.name === allPatternsCategory?.name ) { return true; } if ( - category.name === myPatternsCategory.name && + category.name === myPatternsCategory?.name && pattern.type === INSERTER_PATTERN_TYPES.user ) { return true; } if ( - category.name === starterPatternsCategory.name && + category.name === starterPatternsCategory?.name && pattern.blockTypes?.includes( 'core/post-content' ) ) { return true; @@ -149,7 +149,7 @@ export function PatternCategoryPreviews( { level={ 4 } as="div" > - { category.label } + { category?.label } </Heading> </FlexBlock> <PatternsFilter 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 766082bd7690d9..5c3560d384181f 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 @@ -25,7 +25,7 @@ import { const getShouldDisableSyncFilter = ( sourceFilter ) => sourceFilter !== 'all' && sourceFilter !== 'user'; const getShouldHideSourcesFilter = ( category ) => { - return category.name === myPatternsCategory.name; + return category?.name === myPatternsCategory.name; }; const PATTERN_SOURCE_MENU_OPTIONS = [ @@ -60,7 +60,7 @@ export function PatternsFilter( { // the user may be confused when switching to another category if the haven't explicity set // this filter themselves. const currentPatternSourceFilter = - category.name === myPatternsCategory.name + category?.name === myPatternsCategory.name ? INSERTER_PATTERN_TYPES.user : patternSourceFilter; diff --git a/packages/block-editor/src/components/inserter/category-tabs/index.js b/packages/block-editor/src/components/inserter/category-tabs/index.js index ff0a130f1a8271..7f5f9ba3f65ad6 100644 --- a/packages/block-editor/src/components/inserter/category-tabs/index.js +++ b/packages/block-editor/src/components/inserter/category-tabs/index.js @@ -64,9 +64,10 @@ function CategoryTabs( { <Tabs.Tab key={ category.name } tabId={ category.name } - aria-label={ category.label } aria-current={ - category === selectedCategory ? 'true' : undefined + category.name === selectedCategory?.name + ? 'true' + : undefined } > { category.label } diff --git a/packages/block-editor/src/components/inserter/index.js b/packages/block-editor/src/components/inserter/index.js index 1af81d0231a1a8..59d78a6f0edc6c 100644 --- a/packages/block-editor/src/components/inserter/index.js +++ b/packages/block-editor/src/components/inserter/index.js @@ -29,7 +29,6 @@ const defaultRenderToggle = ( { blockTitle, hasSingleBlockType, toggleProps = {}, - prioritizePatterns, } ) => { const { as: Wrapper = Button, @@ -45,8 +44,6 @@ const defaultRenderToggle = ( { _x( 'Add %s', 'directly add the only allowed block' ), blockTitle ); - } else if ( ! label && prioritizePatterns ) { - label = __( 'Add pattern' ); } else if ( ! label ) { label = _x( 'Add block', 'Generic label for block inserter button' ); } @@ -63,6 +60,7 @@ const defaultRenderToggle = ( { return ( <Wrapper + __next40pxDefaultSize={ toggleProps.as ? undefined : true } icon={ plus } label={ label } tooltipPosition="bottom" @@ -113,7 +111,6 @@ class Inserter extends Component { toggleProps, hasItems, renderToggle = defaultRenderToggle, - prioritizePatterns, } = this.props; return renderToggle( { @@ -124,7 +121,6 @@ class Inserter extends Component { hasSingleBlockType, directInsertBlock, toggleProps, - prioritizePatterns, } ); } @@ -147,7 +143,6 @@ class Inserter extends Component { // This prop is experimental to give some time for the quick inserter to mature // Feel free to make them stable after a few releases. __experimentalIsQuick: isQuick, - prioritizePatterns, onSelectOrClose, selectBlockOnInsert, } = this.props; @@ -171,7 +166,6 @@ class Inserter extends Component { rootClientId={ rootClientId } clientId={ clientId } isAppender={ isAppender } - prioritizePatterns={ prioritizePatterns } selectBlockOnInsert={ selectBlockOnInsert } /> ); @@ -230,7 +224,6 @@ export default compose( [ hasInserterItems, getAllowedBlocks, getDirectInsertBlock, - getSettings, } = select( blockEditorStore ); const { getBlockVariations } = select( blocksStore ); @@ -243,8 +236,6 @@ export default compose( [ const directInsertBlock = shouldDirectInsert && getDirectInsertBlock( rootClientId ); - const settings = getSettings(); - const hasSingleBlockType = allowedBlocks?.length === 1 && getBlockVariations( allowedBlocks[ 0 ].name, 'inserter' ) @@ -262,9 +253,6 @@ export default compose( [ allowedBlockType, directInsertBlock, rootClientId, - prioritizePatterns: - settings.__experimentalPreferPatternsOnRoot && - ! rootClientId, }; } ), diff --git a/packages/block-editor/src/components/inserter/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js index 9f393a7ce15202..498030a0019dcc 100644 --- a/packages/block-editor/src/components/inserter/quick-inserter.js +++ b/packages/block-editor/src/components/inserter/quick-inserter.js @@ -16,21 +16,17 @@ import { useSelect } from '@wordpress/data'; */ import InserterSearchResults from './search-results'; import useInsertionPoint from './hooks/use-insertion-point'; -import usePatternsState from './hooks/use-patterns-state'; import useBlockTypesState from './hooks/use-block-types-state'; import { store as blockEditorStore } from '../../store'; const SEARCH_THRESHOLD = 6; const SHOWN_BLOCK_TYPES = 6; -const SHOWN_BLOCK_PATTERNS = 2; -const SHOWN_BLOCK_PATTERNS_WITH_PRIORITIZATION = 4; export default function QuickInserter( { onSelect, rootClientId, clientId, isAppender, - prioritizePatterns, selectBlockOnInsert, hasSearch = true, } ) { @@ -47,12 +43,6 @@ export default function QuickInserter( { onInsertBlocks, true ); - const [ patterns ] = usePatternsState( - onInsertBlocks, - destinationRootClientId, - undefined, - true - ); const { setInserterIsOpened, insertionIndex } = useSelect( ( select ) => { @@ -70,12 +60,7 @@ export default function QuickInserter( { [ clientId ] ); - const showPatterns = - patterns.length && ( !! filterValue || prioritizePatterns ); - const showSearch = - hasSearch && - ( ( showPatterns && patterns.length > SEARCH_THRESHOLD ) || - blockTypes.length > SEARCH_THRESHOLD ); + const showSearch = hasSearch && blockTypes.length > SEARCH_THRESHOLD; useEffect( () => { if ( setInserterIsOpened ) { @@ -94,13 +79,6 @@ export default function QuickInserter( { } ); }; - let maxBlockPatterns = 0; - if ( showPatterns ) { - maxBlockPatterns = prioritizePatterns - ? SHOWN_BLOCK_PATTERNS_WITH_PRIORITIZATION - : SHOWN_BLOCK_PATTERNS; - } - return ( <div className={ clsx( 'block-editor-inserter__quick-inserter', { @@ -128,10 +106,9 @@ export default function QuickInserter( { rootClientId={ rootClientId } clientId={ clientId } isAppender={ isAppender } - maxBlockPatterns={ maxBlockPatterns } + maxBlockPatterns={ 0 } maxBlockTypes={ SHOWN_BLOCK_TYPES } isDraggable={ false } - prioritizePatterns={ prioritizePatterns } selectBlockOnInsert={ selectBlockOnInsert } isQuick /> diff --git a/packages/block-editor/src/components/keyboard-shortcuts/index.js b/packages/block-editor/src/components/keyboard-shortcuts/index.js index 9b838446469229..fcc2cea4043753 100644 --- a/packages/block-editor/src/components/keyboard-shortcuts/index.js +++ b/packages/block-editor/src/components/keyboard-shortcuts/index.js @@ -29,8 +29,8 @@ function KeyboardShortcutsRegister() { category: 'block', description: __( 'Remove the selected block(s).' ), keyCombination: { - modifier: 'access', - character: 'z', + modifier: 'shift', + character: 'backspace', }, } ); diff --git a/packages/block-editor/src/components/line-height-control/README.md b/packages/block-editor/src/components/line-height-control/README.md index 89bcc69622367f..2f719b5a7210e6 100644 --- a/packages/block-editor/src/components/line-height-control/README.md +++ b/packages/block-editor/src/components/line-height-control/README.md @@ -18,6 +18,7 @@ const MyLineHeightControl = () => ( <LineHeightControl value={ lineHeight } onChange={ onChange } + __next40pxDefaultSize /> ); ``` diff --git a/packages/block-editor/src/components/line-height-control/index.js b/packages/block-editor/src/components/line-height-control/index.js index e6af602c2875ae..ea692ceb452e3a 100644 --- a/packages/block-editor/src/components/line-height-control/index.js +++ b/packages/block-editor/src/components/line-height-control/index.js @@ -3,6 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { __experimentalNumberControl as NumberControl } from '@wordpress/components'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -89,6 +90,17 @@ const LineHeightControl = ( { onChange( `${ nextValue }` ); }; + if ( + ! __next40pxDefaultSize && + ( otherProps.size === undefined || otherProps.size === 'default' ) + ) { + deprecated( `36px default size for wp.blockEditor.LineHeightControl`, { + since: '6.8', + version: '7.1', + hint: 'Set the `__next40pxDefaultSize` prop to true to start opting into the new default size, which will become the default in a future version.', + } ); + } + return ( <div className="block-editor-line-height-control"> <NumberControl diff --git a/packages/block-editor/src/components/line-height-control/stories/index.story.js b/packages/block-editor/src/components/line-height-control/stories/index.story.js index 6d26fe2220fd23..f9f8c7eef12554 100644 --- a/packages/block-editor/src/components/line-height-control/stories/index.story.js +++ b/packages/block-editor/src/components/line-height-control/stories/index.story.js @@ -22,6 +22,7 @@ const Template = ( props ) => { export const Default = Template.bind( {} ); Default.args = { + __next40pxDefaultSize: true, __unstableInputWidth: '100px', }; diff --git a/packages/block-editor/src/components/line-height-control/test/index.js b/packages/block-editor/src/components/line-height-control/test/index.js index b98bc93c48a83a..488d22b768114e 100644 --- a/packages/block-editor/src/components/line-height-control/test/index.js +++ b/packages/block-editor/src/components/line-height-control/test/index.js @@ -19,7 +19,13 @@ const SPIN = STEP * SPIN_FACTOR; const ControlledLineHeightControl = () => { const [ value, setValue ] = useState(); - return <LineHeightControl value={ value } onChange={ setValue } />; + return ( + <LineHeightControl + value={ value } + onChange={ setValue } + __next40pxDefaultSize + /> + ); }; describe( 'LineHeightControl', () => { diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 2916622efabee9..3529c27b56e24f 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -553,13 +553,18 @@ svg { } .list-view-appender .block-editor-inserter__toggle { - background-color: #1e1e1e; - color: #fff; - margin: $grid-unit-10 0 0 24px; - height: 24px; - min-width: 24px; + background-color: $gray-900; + color: $white; + margin: $grid-unit-10 0 0 $grid-unit-30; + height: $button-size-small; padding: 0; + // TODO: Consider passing size="small" to the Inserter toggle instead. + // Special dimensions for this button. + &.has-icon.is-next-40px-default-size { + min-width: $button-size-small; + } + &:hover, &:focus { background: var(--wp-admin-theme-color); diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 0cbc6c8c26203f..b19411893b86ca 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -15,7 +15,7 @@ import { __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper, withFilters, } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { useState, useEffect } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { keyboardReturn } from '@wordpress/icons'; @@ -482,7 +482,7 @@ export function MediaPlaceholder( { ) } onClick={ openFileDialog } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } </Button> { uploadMediaLibraryButton } { renderUrlSelectionUI() } @@ -512,7 +512,7 @@ export function MediaPlaceholder( { 'block-editor-media-placeholder__upload-button' ) } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } </Button> ) } onChange={ onUpload } diff --git a/packages/block-editor/src/components/media-replace-flow/README.md b/packages/block-editor/src/components/media-replace-flow/README.md index a5808ab9561980..b3427efffbcf1f 100644 --- a/packages/block-editor/src/components/media-replace-flow/README.md +++ b/packages/block-editor/src/components/media-replace-flow/README.md @@ -98,3 +98,10 @@ If passed, children are rendered inside the dropdown. - Required: No If passed, children are rendered inside the dropdown. If a function is provided for this prop, it will receive an object with the `onClose` prop as an argument. + +### renderToggle + +- Type: `func` +- Required: No + +If passed, it will be used to render the provided button instead of the default one. It should accept and pass through `button` props to a `button` element. diff --git a/packages/block-editor/src/components/media-replace-flow/index.js b/packages/block-editor/src/components/media-replace-flow/index.js index 0da15033a86bf0..53c2a66634f0ae 100644 --- a/packages/block-editor/src/components/media-replace-flow/index.js +++ b/packages/block-editor/src/components/media-replace-flow/index.js @@ -1,21 +1,15 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - /** * WordPress dependencies */ -import { useRef } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; import { FormFileUpload, NavigableMenu, MenuItem, - ToolbarButton, Dropdown, withFilters, + ToolbarButton, } from '@wordpress/components'; import { useSelect, withDispatch } from '@wordpress/data'; import { DOWN } from '@wordpress/keycodes'; @@ -60,12 +54,9 @@ const MediaReplaceFlow = ( { addToGallery, handleUpload = true, popoverProps, + renderToggle, } ) => { - const mediaUpload = useSelect( ( select ) => { - return select( blockEditorStore ).getSettings().mediaUpload; - }, [] ); - const canUpload = !! mediaUpload; - const editMediaButtonRef = useRef(); + const { getSettings } = useSelect( blockEditorStore ); const errorNoticeID = `block-editor/media-replace-flow/error-notice/${ ++uniqueId }`; const onUploadError = ( message ) => { @@ -107,7 +98,7 @@ const MediaReplaceFlow = ( { return onSelect( files ); } onFilesUpload( files ); - mediaUpload( { + getSettings().mediaUpload( { allowedTypes, filesList: files, onFileChange: ( [ media ] ) => { @@ -141,17 +132,27 @@ const MediaReplaceFlow = ( { <Dropdown popoverProps={ popoverProps } contentClassName="block-editor-media-replace-flow__options" - renderToggle={ ( { isOpen, onToggle } ) => ( - <ToolbarButton - ref={ editMediaButtonRef } - aria-expanded={ isOpen } - aria-haspopup="true" - onClick={ onToggle } - onKeyDown={ openOnArrowDown } - > - { name } - </ToolbarButton> - ) } + renderToggle={ ( { isOpen, onToggle } ) => { + if ( renderToggle ) { + return renderToggle( { + 'aria-expanded': isOpen, + 'aria-haspopup': 'true', + onClick: onToggle, + onKeyDown: openOnArrowDown, + children: name, + } ); + } + return ( + <ToolbarButton + aria-expanded={ isOpen } + aria-haspopup="true" + onClick={ onToggle } + onKeyDown={ openOnArrowDown } + > + { name } + </ToolbarButton> + ); + } } renderContent={ ( { onClose } ) => ( <> <NavigableMenu className="block-editor-media-replace-flow__media-upload-menu"> @@ -188,7 +189,7 @@ const MediaReplaceFlow = ( { openFileDialog(); } } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } </MenuItem> ); } } @@ -219,15 +220,7 @@ const MediaReplaceFlow = ( { </NavigableMenu> { onSelectURL && ( // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions - <form - className={ clsx( - 'block-editor-media-flow__url-input', - { - 'has-siblings': - canUpload || onToggleFeaturedImage, - } - ) } - > + <form className="block-editor-media-flow__url-input"> <span className="block-editor-media-replace-flow__image-url-label"> { __( 'Current media URL:' ) } </span> @@ -238,7 +231,6 @@ const MediaReplaceFlow = ( { showSuggestions={ false } onChange={ ( { url } ) => { onSelectURL( url ); - editMediaButtonRef.current.focus(); } } /> </form> diff --git a/packages/block-editor/src/components/media-replace-flow/style.scss b/packages/block-editor/src/components/media-replace-flow/style.scss index 61df542cf58404..d9d8d1c98c11f5 100644 --- a/packages/block-editor/src/components/media-replace-flow/style.scss +++ b/packages/block-editor/src/components/media-replace-flow/style.scss @@ -9,17 +9,17 @@ margin-left: 4px; } +.block-editor-media-replace-flow__media-upload-menu:not(:empty) + .block-editor-media-flow__url-input { + border-top: $border-width solid $gray-900; + margin-top: $grid-unit-10; + padding-bottom: $grid-unit-10; +} + .block-editor-media-flow__url-input { margin-right: -$grid-unit-10; margin-left: -$grid-unit-10; padding: $grid-unit-20; - &.has-siblings { - border-top: $border-width solid $gray-900; - margin-top: $grid-unit-10; - padding-bottom: $grid-unit-10; - } - .block-editor-media-replace-flow__image-url-label { display: block; top: $grid-unit-20; diff --git a/packages/block-editor/src/components/plain-text/README.md b/packages/block-editor/src/components/plain-text/README.md index aa15758118afdc..1e0a7888ed1e4d 100644 --- a/packages/block-editor/src/components/plain-text/README.md +++ b/packages/block-editor/src/components/plain-text/README.md @@ -6,11 +6,11 @@ Render an auto-growing textarea allow users to fill any textual content. ### `value: string` -_Required._ String value of the textarea +_Required._ String value of the textarea. ### `onChange( value: string ): Function` -_Required._ Called when the value changes. +_Required._ Function called when the text value changes. You can also pass any extra prop to the textarea rendered by this component. diff --git a/packages/block-editor/src/components/plain-text/index.js b/packages/block-editor/src/components/plain-text/index.js index 4bd6681f4eb079..d28aabebf7a140 100644 --- a/packages/block-editor/src/components/plain-text/index.js +++ b/packages/block-editor/src/components/plain-text/index.js @@ -15,7 +15,41 @@ import { forwardRef } from '@wordpress/element'; import EditableText from '../editable-text'; /** + * Render an auto-growing textarea allow users to fill any textual content. + * * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/plain-text/README.md + * + * @example + * ```jsx + * import { registerBlockType } from '@wordpress/blocks'; + * import { PlainText } from '@wordpress/block-editor'; + * + * registerBlockType( 'my-plugin/example-block', { + * // ... + * + * attributes: { + * content: { + * type: 'string', + * }, + * }, + * + * edit( { className, attributes, setAttributes } ) { + * return ( + * <PlainText + * className={ className } + * value={ attributes.content } + * onChange={ ( content ) => setAttributes( { content } ) } + * /> + * ); + * }, + * } ); + * ```` + * + * @param {Object} props Component props. + * @param {string} props.value String value of the textarea. + * @param {Function} props.onChange Function called when the text value changes. + * @param {Object} [props.ref] The component forwards the `ref` property to the `TextareaAutosize` component. + * @return {Element} Plain text component */ const PlainText = forwardRef( ( { __experimentalVersion, ...props }, ref ) => { if ( __experimentalVersion === 2 ) { diff --git a/packages/block-editor/src/components/plain-text/stories/index.story.js b/packages/block-editor/src/components/plain-text/stories/index.story.js new file mode 100644 index 00000000000000..d1a6253c0870a7 --- /dev/null +++ b/packages/block-editor/src/components/plain-text/stories/index.story.js @@ -0,0 +1,75 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import PlainText from '..'; + +const meta = { + title: 'BlockEditor/PlainText', + component: PlainText, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: + 'PlainText renders an auto-growing textarea that allows users to enter any textual content.', + }, + }, + }, + argTypes: { + value: { + control: { + type: null, + }, + table: { + type: { + summary: 'string', + }, + }, + description: 'String value of the textarea.', + }, + onChange: { + action: 'onChange', + control: { + type: null, + }, + table: { + type: { + summary: 'function', + }, + }, + description: 'Function called when the text value changes.', + }, + className: { + control: 'text', + table: { + type: { + summary: 'string', + }, + }, + description: 'Additional class name for the PlainText.', + }, + }, +}; + +export default meta; + +export const Default = { + render: function Template( { onChange, ...args } ) { + const [ value, setValue ] = useState(); + return ( + <PlainText + { ...args } + onChange={ ( ...changeArgs ) => { + onChange( ...changeArgs ); + setValue( ...changeArgs ); + } } + value={ value } + /> + ); + }, +}; diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index abbb122ae3a0e0..97aa0b95216870 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -2,8 +2,13 @@ * WordPress dependencies */ import { useDispatch } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; +import { useEffect, useMemo } from '@wordpress/element'; import { SlotFillProvider } from '@wordpress/components'; +//eslint-disable-next-line import/no-extraneous-dependencies -- Experimental package, not published. +import { + MediaUploadProvider, + store as uploadStore, +} from '@wordpress/upload-media'; /** * Internal dependencies @@ -14,12 +19,71 @@ import { store as blockEditorStore } from '../../store'; import { BlockRefsProvider } from './block-refs-provider'; import { unlock } from '../../lock-unlock'; import KeyboardShortcuts from '../keyboard-shortcuts'; +import useMediaUploadSettings from './use-media-upload-settings'; /** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */ +const noop = () => {}; + +/** + * Upload a media file when the file upload button is activated + * or when adding a file to the editor via drag & drop. + * + * @param {WPDataRegistry} registry + * @param {Object} $3 Parameters object passed to the function. + * @param {Array} $3.allowedTypes Array with the types of media that can be uploaded, if unset all types are allowed. + * @param {Object} $3.additionalData Additional data to include in the request. + * @param {Array<File>} $3.filesList List of files. + * @param {Function} $3.onError Function called when an error happens. + * @param {Function} $3.onFileChange Function called each time a file or a temporary representation of the file is available. + * @param {Function} $3.onSuccess Function called once a file has completely finished uploading, including thumbnails. + * @param {Function} $3.onBatchSuccess Function called once all files in a group have completely finished uploading, including thumbnails. + */ +function mediaUpload( + registry, + { + allowedTypes, + additionalData = {}, + filesList, + onError = noop, + onFileChange, + onSuccess, + onBatchSuccess, + } +) { + void registry.dispatch( uploadStore ).addItems( { + files: filesList, + onChange: onFileChange, + onSuccess, + onBatchSuccess, + onError: ( { message } ) => onError( message ), + additionalData, + allowedTypes, + } ); +} + export const ExperimentalBlockEditorProvider = withRegistryProvider( ( props ) => { - const { children, settings, stripExperimentalSettings = false } = props; + const { + settings: _settings, + registry, + stripExperimentalSettings = false, + } = props; + + const mediaUploadSettings = useMediaUploadSettings( _settings ); + + let settings = _settings; + + if ( window.__experimentalMediaProcessing && _settings.mediaUpload ) { + // Create a new variable so that the original props.settings.mediaUpload is not modified. + settings = useMemo( + () => ( { + ..._settings, + mediaUpload: mediaUpload.bind( null, registry ), + } ), + [ _settings, registry ] + ); + } const { __experimentalUpdateSettings } = unlock( useDispatch( blockEditorStore ) @@ -44,12 +108,25 @@ export const ExperimentalBlockEditorProvider = withRegistryProvider( // Syncs the entity provider with changes in the block-editor store. useBlockSync( props ); - return ( + const children = ( <SlotFillProvider passthrough> { ! settings?.isPreviewMode && <KeyboardShortcuts.Register /> } - <BlockRefsProvider>{ children }</BlockRefsProvider> + <BlockRefsProvider>{ props.children }</BlockRefsProvider> </SlotFillProvider> ); + + if ( window.__experimentalMediaProcessing ) { + return ( + <MediaUploadProvider + settings={ mediaUploadSettings } + useSubRegistry={ false } + > + { children } + </MediaUploadProvider> + ); + } + + return children; } ); diff --git a/packages/block-editor/src/components/provider/use-media-upload-settings.js b/packages/block-editor/src/components/provider/use-media-upload-settings.js new file mode 100644 index 00000000000000..40390a77e746ef --- /dev/null +++ b/packages/block-editor/src/components/provider/use-media-upload-settings.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; + +/** + * React hook used to compute the media upload settings to use in the post editor. + * + * @param {Object} settings Media upload settings prop. + * + * @return {Object} Media upload settings. + */ +function useMediaUploadSettings( settings = {} ) { + return useMemo( + () => ( { + mediaUpload: settings.mediaUpload, + mediaSideload: settings.mediaSideload, + maxUploadFileSize: settings.maxUploadFileSize, + allowedMimeTypes: settings.allowedMimeTypes, + } ), + [ settings ] + ); +} + +export default useMediaUploadSettings; diff --git a/packages/block-editor/src/components/resolution-tool/index.js b/packages/block-editor/src/components/resolution-tool/index.js index df43cb6acb096d..b73a2d5f249723 100644 --- a/packages/block-editor/src/components/resolution-tool/index.js +++ b/packages/block-editor/src/components/resolution-tool/index.js @@ -33,6 +33,7 @@ export default function ResolutionTool( { options = DEFAULT_SIZE_OPTIONS, defaultValue = DEFAULT_SIZE_OPTIONS[ 0 ].value, isShownByDefault = true, + resetAllFilter, } ) { const displayValue = value ?? defaultValue; return ( @@ -42,6 +43,7 @@ export default function ResolutionTool( { onDeselect={ () => onChange( defaultValue ) } isShownByDefault={ isShownByDefault } panelId={ panelId } + resetAllFilter={ resetAllFilter } > <SelectControl __nextHasNoMarginBottom diff --git a/packages/block-editor/src/components/resolution-tool/stories/index.story.js b/packages/block-editor/src/components/resolution-tool/stories/index.story.js index 3fedb6d6facae7..531618b38224f9 100644 --- a/packages/block-editor/src/components/resolution-tool/stories/index.story.js +++ b/packages/block-editor/src/components/resolution-tool/stories/index.story.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useState } from '@wordpress/element'; +import { useReducer } from '@wordpress/element'; import { Panel, __experimentalToolsPanel as ToolsPanel, @@ -13,30 +13,56 @@ import { import ResolutionTool from '..'; export default { - title: 'BlockEditor (Private APIs)/ResolutionControl', + title: 'BlockEditor/ResolutionControl', component: ResolutionTool, + tags: [ 'status-private' ], argTypes: { panelId: { control: false }, onChange: { action: 'changed' }, }, }; -export const Default = ( { panelId, onChange: onChangeProp, ...props } ) => { - const [ resolution, setResolution ] = useState( undefined ); - const resetAll = () => { - setResolution( undefined ); +export const Default = ( { + label, + panelId, + onChange: onChangeProp, + ...props +} ) => { + const [ attributes, setAttributes ] = useReducer( + ( prevState, nextState ) => ( { ...prevState, ...nextState } ), + {} + ); + const { resolution } = attributes; + const resetAll = ( resetFilters = [] ) => { + let newAttributes = {}; + + resetFilters.forEach( ( resetFilter ) => { + newAttributes = { + ...newAttributes, + ...resetFilter( newAttributes ), + }; + } ); + + setAttributes( newAttributes ); onChangeProp( undefined ); }; return ( <Panel> - <ToolsPanel panelId={ panelId } resetAll={ resetAll }> + <ToolsPanel + label={ label } + panelId={ panelId } + resetAll={ resetAll } + > <ResolutionTool panelId={ panelId } onChange={ ( newValue ) => { - setResolution( newValue ); + setAttributes( { resolution: newValue } ); onChangeProp( newValue ); } } value={ resolution } + resetAllFilter={ () => ( { + resolution: undefined, + } ) } { ...props } /> </ToolsPanel> @@ -44,5 +70,7 @@ export const Default = ( { panelId, onChange: onChangeProp, ...props } ) => { ); }; Default.args = { + label: 'Settings', + defaultValue: 'full', panelId: 'panel-id', }; diff --git a/packages/block-editor/src/components/responsive-block-control/index.js b/packages/block-editor/src/components/responsive-block-control/index.js index 148ba9600f0032..388e7ec543693a 100644 --- a/packages/block-editor/src/components/responsive-block-control/index.js +++ b/packages/block-editor/src/components/responsive-block-control/index.js @@ -57,7 +57,7 @@ function ResponsiveBlockControl( props ) { ); const toggleHelpText = __( - 'Toggle between using the same value for all screen sizes or using a unique value per screen size.' + 'Choose whether to use the same value for all screen sizes or a unique value for each screen size.' ); const defaultControl = renderDefaultControl( diff --git a/packages/block-editor/src/components/rich-text/event-listeners/delete.js b/packages/block-editor/src/components/rich-text/event-listeners/delete.js index ae3fd733bb94e1..8373ca3c9f72ae 100644 --- a/packages/block-editor/src/components/rich-text/event-listeners/delete.js +++ b/packages/block-editor/src/components/rich-text/event-listeners/delete.js @@ -6,7 +6,7 @@ import { isCollapsed, isEmpty } from '@wordpress/rich-text'; export default ( props ) => ( element ) => { function onKeyDown( event ) { - const { keyCode } = event; + const { keyCode, shiftKey } = event; if ( event.defaultPrevented ) { return; @@ -30,6 +30,11 @@ export default ( props ) => ( element ) => { return; } + // Exclude shift+backspace as they are shortcuts for deleting blocks. + if ( shiftKey ) { + return; + } + if ( onMerge ) { onMerge( ! isReverse ); } diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index bc8eca6ea94d05..768ffbb0cdd2dc 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -39,7 +39,7 @@ import FormatEdit from './format-edit'; import { getAllowedFormats } from './utils'; import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; -import { canBindBlock } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../utils/block-bindings'; import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); diff --git a/packages/block-editor/src/components/text-alignment-control/README.md b/packages/block-editor/src/components/text-alignment-control/README.md new file mode 100644 index 00000000000000..243a5fec7938b7 --- /dev/null +++ b/packages/block-editor/src/components/text-alignment-control/README.md @@ -0,0 +1,49 @@ +# TextAlignmentControl + +The `TextAlignmentControl` component is responsible for rendering a control element that allows users to select and apply text alignment options to blocks or elements in the Gutenberg editor. It provides an intuitive interface for aligning text with options such as `left`, `center` and `right`. + +## Usage + +Renders the Text Alignment Component with `left`, `center` and `right` alignment options. + +```jsx +import { TextAlignmentControl } from '@wordpress/block-editor'; + +const MyTextAlignmentControlComponent = () => ( + <TextAlignmentControl + value={ textAlign } + onChange={ ( value ) => { + setAttributes( { textAlign: value } ); + } } + /> +); +``` + +## Props + +### `value` + +- **Type:** `String` +- **Default:** `undefined` +- **Options:** `left`, `center`, `right`, `justify` + +The current value of the text alignment setting. You may only choose from the `Options` listed above. + +### `onChange` + +- **Type:** `Function` + +A callback function invoked when the text alignment value is changed via an interaction with any of the options. The function is called with the new alignment value (`left`, `center`, `right`) as the only argument. + +### `className` + +- **Type:** `String` + +Class name to add to the control for custom styling. + +### `options` + +- **Type:** `Array` +- **Default:** [`left`, `center`, `right`] + +An array that determines which alignment options will be available in the control. You can pass an array of alignment values to customize the options. diff --git a/packages/block-editor/src/components/text-alignment-control/stories/index.story.js b/packages/block-editor/src/components/text-alignment-control/stories/index.story.js index 3744f3fa012a71..076535ab330d69 100644 --- a/packages/block-editor/src/components/text-alignment-control/stories/index.story.js +++ b/packages/block-editor/src/components/text-alignment-control/stories/index.story.js @@ -8,32 +8,70 @@ import { useState } from '@wordpress/element'; */ import TextAlignmentControl from '../'; -export default { +const meta = { title: 'BlockEditor/TextAlignmentControl', component: TextAlignmentControl, + tags: [ 'status-private' ], + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: 'Control to facilitate text alignment selections.', + }, + }, + }, argTypes: { - onChange: { action: 'onChange' }, - className: { control: 'text' }, + value: { + control: { type: null }, + description: 'Currently selected text alignment value.', + table: { + type: { + summary: 'string', + }, + }, + }, + onChange: { + action: 'onChange', + control: { type: null }, + description: 'Handles change in text alignment selection.', + table: { + type: { + summary: 'function', + }, + }, + }, options: { control: 'check', + description: 'Array of text alignment options to display.', options: [ 'left', 'center', 'right', 'justify' ], + table: { + type: { summary: 'array' }, + }, + }, + className: { + control: 'text', + description: 'Class name to add to the control.', + table: { + type: { summary: 'string' }, + }, }, - value: { control: false }, }, }; -const Template = ( { onChange, ...args } ) => { - const [ value, setValue ] = useState(); - return ( - <TextAlignmentControl - { ...args } - onChange={ ( ...changeArgs ) => { - onChange( ...changeArgs ); - setValue( ...changeArgs ); - } } - value={ value } - /> - ); -}; +export default meta; -export const Default = Template.bind( {} ); +export const Default = { + render: function Template( { onChange, ...args } ) { + const [ value, setValue ] = useState(); + return ( + <TextAlignmentControl + { ...args } + onChange={ ( ...changeArgs ) => { + onChange( ...changeArgs ); + setValue( ...changeArgs ); + } } + value={ value } + /> + ); + }, +}; diff --git a/packages/block-editor/src/components/text-decoration-control/README.md b/packages/block-editor/src/components/text-decoration-control/README.md index a606140baa330e..87fb6e89bd5712 100644 --- a/packages/block-editor/src/components/text-decoration-control/README.md +++ b/packages/block-editor/src/components/text-decoration-control/README.md @@ -28,7 +28,6 @@ Then, you can use the component in your block editor UI: ### `value` - **Type:** `String` -- **Default:** `none` - **Options:** `none`, `underline`, `line-through` The current value of the Text Decoration setting. You may only choose from the `Options` listed above. diff --git a/packages/block-editor/src/components/text-decoration-control/stories/index.story.js b/packages/block-editor/src/components/text-decoration-control/stories/index.story.js index 2212b484185cde..d139b30a2bb4b5 100644 --- a/packages/block-editor/src/components/text-decoration-control/stories/index.story.js +++ b/packages/block-editor/src/components/text-decoration-control/stories/index.story.js @@ -8,26 +8,61 @@ import { useState } from '@wordpress/element'; */ import TextDecorationControl from '../'; -export default { +const meta = { title: 'BlockEditor/TextDecorationControl', component: TextDecorationControl, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: 'Control to facilitate text decoration selections.', + }, + }, + }, argTypes: { - onChange: { action: 'onChange' }, + value: { + control: { type: null }, + description: 'Currently selected text decoration.', + table: { + type: { + summary: 'string', + }, + }, + }, + onChange: { + action: 'onChange', + control: { type: null }, + description: 'Handles change in text decoration selection.', + table: { + type: { + summary: 'function', + }, + }, + }, + className: { + control: 'text', + description: 'Additional class name to apply.', + table: { + type: { summary: 'string' }, + }, + }, }, }; -const Template = ( { onChange, ...args } ) => { - const [ value, setValue ] = useState(); - return ( - <TextDecorationControl - { ...args } - onChange={ ( ...changeArgs ) => { - onChange( ...changeArgs ); - setValue( ...changeArgs ); - } } - value={ value } - /> - ); -}; +export default meta; -export const Default = Template.bind( {} ); +export const Default = { + render: function Template( { onChange, ...args } ) { + const [ value, setValue ] = useState(); + return ( + <TextDecorationControl + { ...args } + onChange={ ( ...changeArgs ) => { + onChange( ...changeArgs ); + setValue( ...changeArgs ); + } } + value={ value } + /> + ); + }, +}; diff --git a/packages/block-editor/src/components/text-transform-control/README.md b/packages/block-editor/src/components/text-transform-control/README.md index 2d40cc16ba86f8..cd23461d3eb332 100644 --- a/packages/block-editor/src/components/text-transform-control/README.md +++ b/packages/block-editor/src/components/text-transform-control/README.md @@ -1,8 +1,8 @@ # TextTransformControl The `TextTransformControl` component is responsible for rendering a control element that allows users to select and apply text transformation options to blocks or elements in the Gutenberg editor. It provides an intuitive interface for changing the text appearance by applying different transformations such as `none`, `uppercase`, `lowercase`, `capitalize`. - -![TextTransformConrol Element in Inspector Control](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/text-transform-component.png?raw=true) + +![TextTransformControl Element in Inspector Control](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/text-transform-component.png?raw=true) ## Development guidelines @@ -28,7 +28,6 @@ const MyTextTransformControlComponent = () => ( ### `value` - **Type:** `String` -- **Default:** `none` - **Options:** `none`, `uppercase`, `lowercase`, `capitalize` The current value of the Text Transform setting. You may only choose from the `Options` listed above. @@ -37,4 +36,4 @@ The current value of the Text Transform setting. You may only choose from the `O - **Type:** `Function` -A callback function invoked when the Text Transform value is changed via an interaction with any of the buttons. Called with the Text Transform value (`none`, `uppercase`, `lowercase`, `capitalize`) as the only argument. \ No newline at end of file +A callback function invoked when the Text Transform value is changed via an interaction with any of the buttons. Called with the Text Transform value (`none`, `uppercase`, `lowercase`, `capitalize`) as the only argument. diff --git a/packages/block-editor/src/components/text-transform-control/stories/index.story.js b/packages/block-editor/src/components/text-transform-control/stories/index.story.js index 96dd8ed479dc4e..77dc550368da19 100644 --- a/packages/block-editor/src/components/text-transform-control/stories/index.story.js +++ b/packages/block-editor/src/components/text-transform-control/stories/index.story.js @@ -8,26 +8,63 @@ import { useState } from '@wordpress/element'; */ import TextTransformControl from '../'; -export default { +const meta = { title: 'BlockEditor/TextTransformControl', component: TextTransformControl, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: + 'Control to facilitate text transformation selections.', + }, + }, + }, argTypes: { - onChange: { action: 'onChange' }, + onChange: { + action: 'onChange', + control: { + type: null, + }, + description: 'Handles change in text transform selection.', + table: { + type: { + summary: 'function', + }, + }, + }, + className: { + control: { type: 'text' }, + description: 'Class name to add to the control.', + table: { + type: { summary: 'string' }, + }, + }, + value: { + control: { type: null }, + description: 'Currently selected text transform.', + table: { + type: { summary: 'string' }, + }, + }, }, }; -const Template = ( { onChange, ...args } ) => { - const [ value, setValue ] = useState(); - return ( - <TextTransformControl - { ...args } - onChange={ ( ...changeArgs ) => { - onChange( ...changeArgs ); - setValue( ...changeArgs ); - } } - value={ value } - /> - ); -}; +export default meta; + +export const Default = { + render: function Template( { onChange, ...args } ) { + const [ value, setValue ] = useState(); -export const Default = Template.bind( {} ); + return ( + <TextTransformControl + { ...args } + onChange={ ( ...changeArgs ) => { + onChange( ...changeArgs ); + setValue( ...changeArgs ); + } } + value={ value } + /> + ); + }, +}; diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index 221e5ab74ebb2e..529eb199fb76a0 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -456,7 +456,14 @@ export default function useBlockDropZone( { const [ targetIndex, operation, nearestSide ] = dropTargetPosition; - if ( isZoomOut() && operation !== 'insert' ) { + const isTargetIndexEmptyDefaultBlock = + blocksData[ targetIndex ]?.isUnmodifiedDefaultBlock; + + if ( + isZoomOut() && + ! isTargetIndexEmptyDefaultBlock && + operation !== 'insert' + ) { return; } diff --git a/packages/block-editor/src/components/warning/stories/index.story.js b/packages/block-editor/src/components/warning/stories/index.story.js new file mode 100644 index 00000000000000..ee881059f302d7 --- /dev/null +++ b/packages/block-editor/src/components/warning/stories/index.story.js @@ -0,0 +1,86 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import Warning from '../'; + +const meta = { + title: 'BlockEditor/Warning', + component: Warning, + parameters: { + docs: { + canvas: { sourceState: 'shown' }, + description: { + component: + 'Displays a warning message with optional action buttons and secondary actions dropdown.', + }, + }, + }, + argTypes: { + children: { + control: 'text', + description: + 'Intended to represent the block to which the warning pertains.', + table: { + type: { summary: 'string|element' }, + }, + }, + className: { + control: 'text', + description: 'Classes to pass to element.', + table: { + type: { summary: 'string' }, + }, + }, + actions: { + control: 'object', + description: + 'An array of elements to be rendered as action buttons in the warning element.', + table: { + type: { summary: 'Element[]' }, + }, + }, + secondaryActions: { + control: 'object', + description: + 'An array of { title, onClick } to be rendered as options in a dropdown of secondary actions.', + table: { + type: { summary: '{ title: string, onClick: Function }[]' }, + }, + }, + }, +}; + +export default meta; + +export const Default = { + args: { + children: __( 'This block ran into an issue.' ), + }, +}; + +export const WithActions = { + args: { + ...Default.args, + actions: [ + <Button key="fix-issue" __next40pxDefaultSize variant="primary"> + { __( 'Fix issue' ) } + </Button>, + ], + }, +}; + +export const WithSecondaryActions = { + args: { + ...Default.args, + secondaryActions: [ + { title: __( 'Get help' ) }, + { title: __( 'Remove block' ) }, + ], + }, +}; diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 3755aecbcb9d0b..6268ff31b29890 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -177,6 +177,11 @@ export function BackgroundImagePanel( { }, }; + const defaultControls = getBlockSupport( name, [ + BACKGROUND_SUPPORT_KEY, + 'defaultControls', + ] ); + return ( <StylesBackgroundPanel inheritedValue={ inheritedValue } @@ -185,6 +190,7 @@ export function BackgroundImagePanel( { defaultValues={ BACKGROUND_BLOCK_DEFAULT_VALUES } settings={ updatedSettings } onChange={ onChange } + defaultControls={ defaultControls } value={ style } /> ); diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index e10696cc1257d7..11e17aba3b30da 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -26,12 +26,12 @@ import { useViewportMatch } from '@wordpress/compose'; import { canBindAttribute, getBindableAttributes, -} from '../hooks/use-bindings-attributes'; + useBlockBindingsUtils, +} from '../utils/block-bindings'; import { unlock } from '../lock-unlock'; import InspectorControls from '../components/inspector-controls'; import BlockContext from '../components/block-context'; import { useBlockEditContext } from '../components/block-edit'; -import { useBlockBindingsUtils } from '../utils/block-bindings'; import { store as blockEditorStore } from '../store'; const { Menu } = unlock( componentsPrivateApis ); @@ -51,7 +51,7 @@ const useToolsPanelDropdownMenuProps = () => { : {}; }; -function BlockBindingsPanelDropdown( { fieldsList, attribute, binding } ) { +function BlockBindingsPanelMenuContent( { fieldsList, attribute, binding } ) { const { clientId } = useBlockEditContext(); const registeredSources = getBlockBindingsSources(); const { updateBlockBindings } = useBlockBindingsUtils(); @@ -179,22 +179,21 @@ function EditableBlockBindingsPanelItems( { placement={ isMobile ? 'bottom-start' : 'left-start' } - gutter={ isMobile ? 8 : 36 } - trigger={ - <Item> - <BlockBindingsAttribute - attribute={ attribute } - binding={ binding } - fieldsList={ fieldsList } - /> - </Item> - } > - <BlockBindingsPanelDropdown - fieldsList={ fieldsList } - attribute={ attribute } - binding={ binding } - /> + <Menu.TriggerButton render={ <Item /> }> + <BlockBindingsAttribute + attribute={ attribute } + binding={ binding } + fieldsList={ fieldsList } + /> + </Menu.TriggerButton> + <Menu.Popover gutter={ isMobile ? 8 : 36 }> + <BlockBindingsPanelMenuContent + fieldsList={ fieldsList } + attribute={ attribute } + binding={ binding } + /> + </Menu.Popover> </Menu> </ToolsPanelItem> ); diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index 14b3dbf7669b3a..4ab4c69a41f311 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -31,7 +31,7 @@ import { import { store as blockEditorStore } from '../store'; import { __ } from '@wordpress/i18n'; -export const BORDER_SUPPORT_KEY = 'border'; +export const BORDER_SUPPORT_KEY = '__experimentalBorder'; export const SHADOW_SUPPORT_KEY = 'shadow'; const getColorByProperty = ( colors, property, value ) => { @@ -161,8 +161,14 @@ export function BorderPanel( { clientId, name, setAttributes, settings } ) { } const defaultControls = { - ...getBlockSupport( name, [ BORDER_SUPPORT_KEY, 'defaultControls' ] ), - ...getBlockSupport( name, [ SHADOW_SUPPORT_KEY, 'defaultControls' ] ), + ...getBlockSupport( name, [ + BORDER_SUPPORT_KEY, + '__experimentalDefaultControls', + ] ), + ...getBlockSupport( name, [ + SHADOW_SUPPORT_KEY, + '__experimentalDefaultControls', + ] ), }; return ( diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 2fecc10a311984..ef8984c9367853 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -290,7 +290,7 @@ export function ColorEdit( { clientId, name, setAttributes, settings } ) { const defaultControls = getBlockSupport( name, [ COLOR_SUPPORT_KEY, - 'defaultControls', + '__experimentalDefaultControls', ] ); const enableContrastChecking = diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index c98cc34e4272c8..ffa4048b7740e3 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -88,11 +88,11 @@ export function DimensionsPanel( { clientId, name, setAttributes, settings } ) { const defaultDimensionsControls = getBlockSupport( name, [ DIMENSIONS_SUPPORT_KEY, - 'defaultControls', + '__experimentalDefaultControls', ] ); const defaultSpacingControls = getBlockSupport( name, [ SPACING_SUPPORT_KEY, - 'defaultControls', + '__experimentalDefaultControls', ] ); const defaultControls = { ...defaultDimensionsControls, diff --git a/packages/block-editor/src/hooks/font-family.js b/packages/block-editor/src/hooks/font-family.js index e5d8e02ab8ec02..ba9a66a8bcf04f 100644 --- a/packages/block-editor/src/hooks/font-family.js +++ b/packages/block-editor/src/hooks/font-family.js @@ -13,7 +13,7 @@ import { shouldSkipSerialization } from './utils'; import { TYPOGRAPHY_SUPPORT_KEY } from './typography'; import { unlock } from '../lock-unlock'; -export const FONT_FAMILY_SUPPORT_KEY = 'typography.fontFamily'; +export const FONT_FAMILY_SUPPORT_KEY = 'typography.__experimentalFontFamily'; const { kebabCase } = unlock( componentsPrivateApis ); /** diff --git a/packages/block-editor/src/hooks/gap.js b/packages/block-editor/src/hooks/gap.js index 887325e6409dde..c5c4bbb7130350 100644 --- a/packages/block-editor/src/hooks/gap.js +++ b/packages/block-editor/src/hooks/gap.js @@ -8,7 +8,7 @@ import { getSpacingPresetCssVar } from '../components/spacing-sizes-control/util * The string check is for backwards compatibility before Gutenberg supported * split gap values (row and column) and the value was a string n + unit. * - * @param {string? | Object?} blockGapValue A block gap string or axial object value, e.g., '10px' or { top: '10px', left: '10px'}. + * @param {?string | ?Object} blockGapValue A block gap string or axial object value, e.g., '10px' or { top: '10px', left: '10px'}. * @return {Object|null} A value to pass to the BoxControl component. */ export function getGapBoxControlValueFromStyle( blockGapValue ) { @@ -26,7 +26,7 @@ export function getGapBoxControlValueFromStyle( blockGapValue ) { /** * Returns a CSS value for the `gap` property from a given blockGap style. * - * @param {string? | Object?} blockGapValue A block gap string or axial object value, e.g., '10px' or { top: '10px', left: '10px'}. + * @param {?string | ?Object} blockGapValue A block gap string or axial object value, e.g., '10px' or { top: '10px', left: '10px'}. * @param {?string} defaultValue A default gap value. * @return {string|null} The concatenated gap value (row and column). */ diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 66ff60b691b66f..7f9b29376ad1fb 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -32,7 +32,6 @@ import './metadata'; import blockHooks from './block-hooks'; import blockBindingsPanel from './block-bindings'; import './block-renaming'; -import './use-bindings-attributes'; import './grid-visualizer'; createBlockEditFilter( diff --git a/packages/block-editor/src/hooks/index.native.js b/packages/block-editor/src/hooks/index.native.js index c7f9df868f2bd7..0e4c2aa276fd40 100644 --- a/packages/block-editor/src/hooks/index.native.js +++ b/packages/block-editor/src/hooks/index.native.js @@ -33,3 +33,4 @@ export { getColorClassesAndStyles, useColorProps } from './use-color-props'; export { getSpacingClassesAndStyles } from './use-spacing-props'; export { useCachedTruthy } from './use-cached-truthy'; export { useEditorWrapperStyles } from './use-editor-wrapper-styles'; +export { getTypographyClassesAndStyles } from './use-typography-props'; diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 5be2b1b3fd40a8..998d13cfd22247 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -98,16 +98,22 @@ function addAttribute( settings ) { * @type {Record<string, string[]>} */ const skipSerializationPathsEdit = { - [ `${ BORDER_SUPPORT_KEY }.skipSerialization` ]: [ 'border' ], - [ `${ COLOR_SUPPORT_KEY }.skipSerialization` ]: [ COLOR_SUPPORT_KEY ], - [ `${ TYPOGRAPHY_SUPPORT_KEY }.skipSerialization` ]: [ + [ `${ BORDER_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ 'border' ], + [ `${ COLOR_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ + COLOR_SUPPORT_KEY, + ], + [ `${ TYPOGRAPHY_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ TYPOGRAPHY_SUPPORT_KEY, ], - [ `${ DIMENSIONS_SUPPORT_KEY }.skipSerialization` ]: [ + [ `${ DIMENSIONS_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ DIMENSIONS_SUPPORT_KEY, ], - [ `${ SPACING_SUPPORT_KEY }.skipSerialization` ]: [ SPACING_SUPPORT_KEY ], - [ `${ SHADOW_SUPPORT_KEY }.skipSerialization` ]: [ SHADOW_SUPPORT_KEY ], + [ `${ SPACING_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ + SPACING_SUPPORT_KEY, + ], + [ `${ SHADOW_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ + SHADOW_SUPPORT_KEY, + ], }; /** @@ -245,7 +251,7 @@ export function omitStyle( style, paths, preserveReference = false ) { let newStyle = style; if ( ! preserveReference ) { - newStyle = structuredClone( style ); + newStyle = JSON.parse( JSON.stringify( style ) ); } if ( ! Array.isArray( paths ) ) { diff --git a/packages/block-editor/src/hooks/supports.js b/packages/block-editor/src/hooks/supports.js index 102b78bbb96e68..75f2bdf2dc219e 100644 --- a/packages/block-editor/src/hooks/supports.js +++ b/packages/block-editor/src/hooks/supports.js @@ -6,20 +6,20 @@ import { Platform } from '@wordpress/element'; const ALIGN_SUPPORT_KEY = 'align'; const ALIGN_WIDE_SUPPORT_KEY = 'alignWide'; -const BORDER_SUPPORT_KEY = 'border'; +const BORDER_SUPPORT_KEY = '__experimentalBorder'; const COLOR_SUPPORT_KEY = 'color'; const CUSTOM_CLASS_NAME_SUPPORT_KEY = 'customClassName'; -const FONT_FAMILY_SUPPORT_KEY = 'typography.fontFamily'; +const FONT_FAMILY_SUPPORT_KEY = 'typography.__experimentalFontFamily'; const FONT_SIZE_SUPPORT_KEY = 'typography.fontSize'; const LINE_HEIGHT_SUPPORT_KEY = 'typography.lineHeight'; /** * Key within block settings' support array indicating support for font style. */ -const FONT_STYLE_SUPPORT_KEY = 'typography.fontStyle'; +const FONT_STYLE_SUPPORT_KEY = 'typography.__experimentalFontStyle'; /** * Key within block settings' support array indicating support for font weight. */ -const FONT_WEIGHT_SUPPORT_KEY = 'typography.fontWeight'; +const FONT_WEIGHT_SUPPORT_KEY = 'typography.__experimentalFontWeight'; /** * Key within block settings' supports array indicating support for text * align e.g. settings found in `block.json`. @@ -34,7 +34,7 @@ const TEXT_COLUMNS_SUPPORT_KEY = 'typography.textColumns'; * Key within block settings' supports array indicating support for text * decorations e.g. settings found in `block.json`. */ -const TEXT_DECORATION_SUPPORT_KEY = 'typography.textDecoration'; +const TEXT_DECORATION_SUPPORT_KEY = 'typography.__experimentalTextDecoration'; /** * Key within block settings' supports array indicating support for writing mode * e.g. settings found in `block.json`. @@ -44,13 +44,13 @@ const WRITING_MODE_SUPPORT_KEY = 'typography.__experimentalWritingMode'; * Key within block settings' supports array indicating support for text * transforms e.g. settings found in `block.json`. */ -const TEXT_TRANSFORM_SUPPORT_KEY = 'typography.textTransform'; +const TEXT_TRANSFORM_SUPPORT_KEY = 'typography.__experimentalTextTransform'; /** * Key within block settings' supports array indicating support for letter-spacing * e.g. settings found in `block.json`. */ -const LETTER_SPACING_SUPPORT_KEY = 'typography.letterSpacing'; +const LETTER_SPACING_SUPPORT_KEY = 'typography.__experimentalLetterSpacing'; const LAYOUT_SUPPORT_KEY = 'layout'; const TYPOGRAPHY_SUPPORT_KEYS = [ LINE_HEIGHT_SUPPORT_KEY, diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js index 40e7169194b82e..2cfe299b8c8d91 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -133,7 +133,8 @@ describe( 'addSaveProps', () => { const applySkipSerialization = ( features ) => { const updatedSettings = { ...blockSettings }; Object.keys( features ).forEach( ( key ) => { - updatedSettings.supports[ key ].skipSerialization = features[ key ]; + updatedSettings.supports[ key ].__experimentalSkipSerialization = + features[ key ]; } ); return updatedSettings; }; diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index 160894eac4e610..cf3f4327c8f034 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -27,12 +27,12 @@ function omit( object, keys ) { ); } -const LETTER_SPACING_SUPPORT_KEY = 'typography.letterSpacing'; -const TEXT_TRANSFORM_SUPPORT_KEY = 'typography.textTransform'; -const TEXT_DECORATION_SUPPORT_KEY = 'typography.textDecoration'; +const LETTER_SPACING_SUPPORT_KEY = 'typography.__experimentalLetterSpacing'; +const TEXT_TRANSFORM_SUPPORT_KEY = 'typography.__experimentalTextTransform'; +const TEXT_DECORATION_SUPPORT_KEY = 'typography.__experimentalTextDecoration'; const TEXT_COLUMNS_SUPPORT_KEY = 'typography.textColumns'; -const FONT_STYLE_SUPPORT_KEY = 'typography.fontStyle'; -const FONT_WEIGHT_SUPPORT_KEY = 'typography.fontWeight'; +const FONT_STYLE_SUPPORT_KEY = 'typography.__experimentalFontStyle'; +const FONT_WEIGHT_SUPPORT_KEY = 'typography.__experimentalFontWeight'; const WRITING_MODE_SUPPORT_KEY = 'typography.__experimentalWritingMode'; export const TYPOGRAPHY_SUPPORT_KEY = 'typography'; export const TYPOGRAPHY_SUPPORT_KEYS = [ @@ -133,7 +133,7 @@ export function TypographyPanel( { clientId, name, setAttributes, settings } ) { const defaultControls = getBlockSupport( name, [ TYPOGRAPHY_SUPPORT_KEY, - 'defaultControls', + '__experimentalDefaultControls', ] ); return ( diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js deleted file mode 100644 index fdc617fda20c05..00000000000000 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ /dev/null @@ -1,322 +0,0 @@ -/** - * WordPress dependencies - */ -import { store as blocksStore } from '@wordpress/blocks'; -import { createHigherOrderComponent } from '@wordpress/compose'; -import { useRegistry, useSelect } from '@wordpress/data'; -import { useCallback, useMemo, useContext } from '@wordpress/element'; -import { addFilter } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import isURLLike from '../components/link-control/is-url-like'; -import { unlock } from '../lock-unlock'; -import BlockContext from '../components/block-context'; - -/** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ -/** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ - -/** - * Given a binding of block attributes, returns a higher order component that - * overrides its `attributes` and `setAttributes` props to sync any changes needed. - * - * @return {WPHigherOrderComponent} Higher-order component. - */ - -const BLOCK_BINDINGS_ALLOWED_BLOCKS = { - 'core/paragraph': [ 'content' ], - 'core/heading': [ 'content' ], - 'core/image': [ 'id', 'url', 'title', 'alt' ], - 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], -}; - -const DEFAULT_ATTRIBUTE = '__default'; - -/** - * Returns the bindings with the `__default` binding for pattern overrides - * replaced with the full-set of supported attributes. e.g.: - * - * bindings passed in: `{ __default: { source: 'core/pattern-overrides' } }` - * bindings returned: `{ content: { source: 'core/pattern-overrides' } }` - * - * @param {string} blockName The block name (e.g. 'core/paragraph'). - * @param {Object} bindings A block's bindings from the metadata attribute. - * - * @return {Object} The bindings with default replaced for pattern overrides. - */ -function replacePatternOverrideDefaultBindings( blockName, bindings ) { - // The `__default` binding currently only works for pattern overrides. - if ( - bindings?.[ DEFAULT_ATTRIBUTE ]?.source === 'core/pattern-overrides' - ) { - const supportedAttributes = BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; - const bindingsWithDefaults = {}; - for ( const attributeName of supportedAttributes ) { - // If the block has mixed binding sources, retain any non pattern override bindings. - const bindingSource = bindings[ attributeName ] - ? bindings[ attributeName ] - : { source: 'core/pattern-overrides' }; - bindingsWithDefaults[ attributeName ] = bindingSource; - } - - return bindingsWithDefaults; - } - - return bindings; -} - -/** - * Based on the given block name, - * check if it is possible to bind the block. - * - * @param {string} blockName - The block name. - * @return {boolean} Whether it is possible to bind the block to sources. - */ -export function canBindBlock( blockName ) { - return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; -} - -/** - * Based on the given block name and attribute name, - * check if it is possible to bind the block attribute. - * - * @param {string} blockName - The block name. - * @param {string} attributeName - The attribute name. - * @return {boolean} Whether it is possible to bind the block attribute. - */ -export function canBindAttribute( blockName, attributeName ) { - return ( - canBindBlock( blockName ) && - BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName ) - ); -} - -export function getBindableAttributes( blockName ) { - return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; -} - -export const withBlockBindingSupport = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const registry = useRegistry(); - const blockContext = useContext( BlockContext ); - const sources = useSelect( ( select ) => - unlock( select( blocksStore ) ).getAllBlockBindingsSources() - ); - const { name, clientId, context, setAttributes } = props; - const blockBindings = useMemo( - () => - replacePatternOverrideDefaultBindings( - name, - props.attributes.metadata?.bindings - ), - [ props.attributes.metadata?.bindings, name ] - ); - - // While this hook doesn't directly call any selectors, `useSelect` is - // used purposely here to ensure `boundAttributes` is updated whenever - // there are attribute updates. - // `source.getValues` may also call a selector via `registry.select`. - const updatedContext = {}; - const boundAttributes = useSelect( - ( select ) => { - if ( ! blockBindings ) { - return; - } - - const attributes = {}; - - const blockBindingsBySource = new Map(); - - for ( const [ attributeName, binding ] of Object.entries( - blockBindings - ) ) { - const { source: sourceName, args: sourceArgs } = binding; - const source = sources[ sourceName ]; - if ( - ! source || - ! canBindAttribute( name, attributeName ) - ) { - continue; - } - - // Populate context. - for ( const key of source.usesContext || [] ) { - updatedContext[ key ] = blockContext[ key ]; - } - - blockBindingsBySource.set( source, { - ...blockBindingsBySource.get( source ), - [ attributeName ]: { - args: sourceArgs, - }, - } ); - } - - if ( blockBindingsBySource.size ) { - for ( const [ - source, - bindings, - ] of blockBindingsBySource ) { - // Get values in batch if the source supports it. - let values = {}; - if ( ! source.getValues ) { - Object.keys( bindings ).forEach( ( attr ) => { - // Default to the the source label when `getValues` doesn't exist. - values[ attr ] = source.label; - } ); - } else { - values = source.getValues( { - select, - context: updatedContext, - clientId, - bindings, - } ); - } - for ( const [ attributeName, value ] of Object.entries( - values - ) ) { - if ( - attributeName === 'url' && - ( ! value || ! isURLLike( value ) ) - ) { - // Return null if value is not a valid URL. - attributes[ attributeName ] = null; - } else { - attributes[ attributeName ] = value; - } - } - } - } - - return attributes; - }, - [ blockBindings, name, clientId, updatedContext, sources ] - ); - - const hasParentPattern = !! updatedContext[ 'pattern/overrides' ]; - const hasPatternOverridesDefaultBinding = - props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] - ?.source === 'core/pattern-overrides'; - - const _setAttributes = useCallback( - ( nextAttributes ) => { - registry.batch( () => { - if ( ! blockBindings ) { - setAttributes( nextAttributes ); - return; - } - - const keptAttributes = { ...nextAttributes }; - const blockBindingsBySource = new Map(); - - // Loop only over the updated attributes to avoid modifying the bound ones that haven't changed. - for ( const [ attributeName, newValue ] of Object.entries( - keptAttributes - ) ) { - if ( - ! blockBindings[ attributeName ] || - ! canBindAttribute( name, attributeName ) - ) { - continue; - } - - const binding = blockBindings[ attributeName ]; - const source = sources[ binding?.source ]; - if ( ! source?.setValues ) { - continue; - } - blockBindingsBySource.set( source, { - ...blockBindingsBySource.get( source ), - [ attributeName ]: { - args: binding.args, - newValue, - }, - } ); - delete keptAttributes[ attributeName ]; - } - - if ( blockBindingsBySource.size ) { - for ( const [ - source, - bindings, - ] of blockBindingsBySource ) { - source.setValues( { - select: registry.select, - dispatch: registry.dispatch, - context: updatedContext, - clientId, - bindings, - } ); - } - } - - if ( - // Don't update non-connected attributes if the block is using pattern overrides - // and the editing is happening while overriding the pattern (not editing the original). - ! ( - hasPatternOverridesDefaultBinding && - hasParentPattern - ) && - Object.keys( keptAttributes ).length - ) { - // Don't update caption and href until they are supported. - if ( hasPatternOverridesDefaultBinding ) { - delete keptAttributes?.caption; - delete keptAttributes?.href; - } - setAttributes( keptAttributes ); - } - } ); - }, - [ - registry, - blockBindings, - name, - clientId, - updatedContext, - setAttributes, - sources, - hasPatternOverridesDefaultBinding, - hasParentPattern, - ] - ); - - return ( - <> - <BlockEdit - { ...props } - attributes={ { ...props.attributes, ...boundAttributes } } - setAttributes={ _setAttributes } - context={ { ...context, ...updatedContext } } - /> - </> - ); - }, - 'withBlockBindingSupport' -); - -/** - * Filters a registered block's settings to enhance a block's `edit` component - * to upgrade bound attributes. - * - * @param {WPBlockSettings} settings - Registered block settings. - * @param {string} name - Block name. - * @return {WPBlockSettings} Filtered block settings. - */ -function shimAttributeSource( settings, name ) { - if ( ! canBindBlock( name ) ) { - return settings; - } - - return { - ...settings, - edit: withBlockBindingSupport( settings.edit ), - }; -} - -addFilter( - 'blocks.registerBlockType', - 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', - shimAttributeSource -); diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js index adcea8b605aeb7..5c37822eba4b38 100644 --- a/packages/block-editor/src/hooks/use-zoom-out.js +++ b/packages/block-editor/src/hooks/use-zoom-out.js @@ -2,13 +2,14 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect, useRef, useContext } from '@wordpress/element'; /** * Internal dependencies */ import { store as blockEditorStore } from '../store'; import { unlock } from '../lock-unlock'; +import BlockContext from '../components/block-context'; /** * A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode. @@ -19,6 +20,7 @@ import { unlock } from '../lock-unlock'; * @param {boolean} enabled If we should enter into zoomOut mode or not */ export function useZoomOut( enabled = true ) { + const { postId } = useContext( BlockContext ); const { setZoomLevel, resetZoomLevel } = unlock( useDispatch( blockEditorStore ) ); @@ -37,6 +39,7 @@ export function useZoomOut( enabled = true ) { const controlZoomLevelRef = useRef( false ); const isEnabledRef = useRef( enabled ); + const postIdRef = useRef( postId ); /** * This hook tracks if the zoom state was changed manually by the user via clicking @@ -55,6 +58,11 @@ export function useZoomOut( enabled = true ) { useEffect( () => { isEnabledRef.current = enabled; + // If the user created a new post/page, we should take control of the zoom level. + if ( postIdRef.current !== postId ) { + controlZoomLevelRef.current = true; + } + if ( enabled !== isZoomOut() ) { controlZoomLevelRef.current = true; @@ -71,5 +79,5 @@ export function useZoomOut( enabled = true ) { resetZoomLevel(); } }; - }, [ enabled, isZoomOut, resetZoomLevel, setZoomLevel ] ); + }, [ enabled, isZoomOut, postId, resetZoomLevel, setZoomLevel ] ); } diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index ac6e55efe4d3bd..4334f70b9d13bf 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -124,7 +124,7 @@ export function shouldSkipSerialization( feature ) { const support = getBlockSupport( blockNameOrType, featureSet ); - const skipSerialization = support?.skipSerialization; + const skipSerialization = support?.__experimentalSkipSerialization; if ( Array.isArray( skipSerialization ) ) { return skipSerialization.includes( feature ); diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index e79833e0a73da7..f085eb2807c6fd 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -26,6 +26,7 @@ const castArray = ( maybeArray ) => const privateSettings = [ 'inserterMediaCategories', 'blockInspectorAnimation', + 'mediaSideload', ]; /** diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index c46778d889b3e0..72b87a59e8f571 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -502,13 +502,23 @@ export const getParentSectionBlock = ( state, clientId ) => { * @return {boolean} Whether the block is a content locking parent. */ export function isSectionBlock( state, clientId ) { + const blockName = getBlockName( state, clientId ); + if ( + blockName === 'core/block' || + getTemplateLock( state, clientId ) === 'contentOnly' + ) { + return true; + } + + // Template parts become sections in navigation mode. + const _isNavigationMode = isNavigationMode( state ); + if ( _isNavigationMode && blockName === 'core/template-part' ) { + return true; + } + const sectionRootClientId = getSectionRootClientId( state ); const sectionClientIds = getBlockOrder( state, sectionRootClientId ); - return ( - getBlockName( state, clientId ) === 'core/block' || - getTemplateLock( state, clientId ) === 'contentOnly' || - ( isNavigationMode( state ) && sectionClientIds.includes( clientId ) ) - ); + return _isNavigationMode && sectionClientIds.includes( clientId ); } /** diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index edae9c392c37de..fc3803462d8920 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1964,8 +1964,14 @@ export function temporarilyEditingFocusModeRevert( state = '', action ) { export function blockEditingModes( state = new Map(), action ) { switch ( action.type ) { case 'SET_BLOCK_EDITING_MODE': + if ( state.get( action.clientId ) === action.mode ) { + return state; + } return new Map( state ).set( action.clientId, action.mode ); case 'UNSET_BLOCK_EDITING_MODE': { + if ( ! state.has( action.clientId ) ) { + return state; + } const newState = new Map( state ); newState.delete( action.clientId ); return newState; @@ -2186,19 +2192,19 @@ function getBlockTreeBlock( state, clientId ) { * The callback receives the current block as its argument. */ function traverseBlockTree( state, clientId, callback ) { - const parentTree = getBlockTreeBlock( state, clientId ); - if ( ! parentTree ) { + const tree = getBlockTreeBlock( state, clientId ); + if ( ! tree ) { return; } - callback( parentTree ); + callback( tree ); - if ( ! parentTree?.innerBlocks?.length ) { + if ( ! tree?.innerBlocks?.length ) { return; } - for ( const block of parentTree?.innerBlocks ) { - traverseBlockTree( state, block.clientId, callback ); + for ( const innerBlock of tree?.innerBlocks ) { + traverseBlockTree( state, innerBlock.clientId, callback ); } } @@ -2212,8 +2218,12 @@ function traverseBlockTree( state, clientId, callback ) { * @return {string|undefined} The client ID of the parent block if found, undefined otherwise. */ function findParentInClientIdsList( state, clientId, clientIds ) { + if ( ! clientIds.length ) { + return; + } + let parent = state.blocks.parents.get( clientId ); - while ( parent ) { + while ( parent !== undefined ) { if ( clientIds.includes( parent ) ) { return parent; } @@ -2258,15 +2268,65 @@ function getDerivedBlockEditingModesForTree( // so the default block editing mode is set to disabled. const sectionRootClientId = state.settings?.[ sectionRootClientIdKey ]; const sectionClientIds = state.blocks.order.get( sectionRootClientId ); - const syncedPatternClientIds = Object.keys( - state.blocks.controlledInnerBlocks - ).filter( - ( clientId ) => - state.blocks.byClientId?.get( clientId )?.name === 'core/block' + const hasDisabledBlocks = Array.from( state.blockEditingModes ).some( + ( [ , mode ] ) => mode === 'disabled' ); + const templatePartClientIds = []; + const syncedPatternClientIds = []; + + Object.keys( state.blocks.controlledInnerBlocks ).forEach( ( clientId ) => { + const block = state.blocks.byClientId?.get( clientId ); + + if ( block?.name === 'core/template-part' ) { + templatePartClientIds.push( clientId ); + } + + if ( block?.name === 'core/block' ) { + syncedPatternClientIds.push( clientId ); + } + } ); traverseBlockTree( state, treeClientId, ( block ) => { const { clientId, name: blockName } = block; + + // If the block already has an explicit block editing mode set, + // don't override it. + if ( state.blockEditingModes.has( clientId ) ) { + return; + } + + // Disabled explicit block editing modes are inherited by children. + // It's an expensive calculation, so only do it if there are disabled blocks. + if ( hasDisabledBlocks ) { + // Look through parents to find one with an explicit block editing mode. + let ancestorBlockEditingMode; + let parent = state.blocks.parents.get( clientId ); + while ( parent !== undefined ) { + // There's a chance we only just calculated this for the parent, + // if so we can return that value for a faster lookup. + if ( derivedBlockEditingModes.has( parent ) ) { + ancestorBlockEditingMode = + derivedBlockEditingModes.get( parent ); + } else if ( state.blockEditingModes.has( parent ) ) { + // Checking the explicit block editing mode will be slower, + // as the block editing mode is more likely to be set on a + // distant ancestor. + ancestorBlockEditingMode = + state.blockEditingModes.get( parent ); + } + if ( ancestorBlockEditingMode ) { + break; + } + parent = state.blocks.parents.get( parent ); + } + + // If the ancestor block editing mode is disabled, it's inherited by the child. + if ( ancestorBlockEditingMode === 'disabled' ) { + derivedBlockEditingModes.set( clientId, 'disabled' ); + return; + } + } + if ( isZoomedOut || isNavMode ) { // If the root block is the section root set its editing mode to contentOnly. if ( clientId === sectionRootClientId ) { @@ -2287,15 +2347,41 @@ function getDerivedBlockEditingModesForTree( // If zoomed out, all blocks that aren't sections or the section root are // disabled. - // If the tree root is not in a section, set its editing mode to disabled. - if ( - isZoomedOut || - ! findParentInClientIdsList( state, clientId, sectionClientIds ) - ) { + if ( isZoomedOut ) { derivedBlockEditingModes.set( clientId, 'disabled' ); return; } + const isInSection = !! findParentInClientIdsList( + state, + clientId, + sectionClientIds + ); + if ( ! isInSection ) { + if ( clientId === '' ) { + derivedBlockEditingModes.set( clientId, 'disabled' ); + return; + } + + // Allow selection of template parts outside of sections. + if ( blockName === 'core/template-part' ) { + derivedBlockEditingModes.set( clientId, 'contentOnly' ); + return; + } + + const isInTemplatePart = !! findParentInClientIdsList( + state, + clientId, + templatePartClientIds + ); + // Allow contentOnly blocks in template parts outside of sections + // to be editable. Only disable blocks that don't fit this criteria. + if ( ! isInTemplatePart && ! isContentBlock( blockName ) ) { + derivedBlockEditingModes.set( clientId, 'disabled' ); + return; + } + } + // Handle synced pattern content so the inner blocks of a synced pattern are // properly disabled. if ( syncedPatternClientIds.length ) { @@ -2560,11 +2646,16 @@ export function withDerivedBlockEditingModes( reducer ) { } break; } + case 'SET_BLOCK_EDITING_MODE': + case 'UNSET_BLOCK_EDITING_MODE': case 'SET_HAS_CONTROLLED_INNER_BLOCKS': { - const updatedBlock = nextState.blocks.tree.get( + const updatedBlock = getBlockTreeBlock( + nextState, action.clientId ); - // The block might have been removed. + + // The block might have been removed in which case it'll be + // handled by the `REMOVE_BLOCKS` action. if ( ! updatedBlock ) { break; } @@ -2573,6 +2664,7 @@ export function withDerivedBlockEditingModes( reducer ) { getDerivedBlockEditingModesUpdates( { prevState: state, nextState, + removedClientIds: [ action.clientId ], addedBlocks: [ updatedBlock ], isNavMode: false, } ); @@ -2580,6 +2672,7 @@ export function withDerivedBlockEditingModes( reducer ) { getDerivedBlockEditingModesUpdates( { prevState: state, nextState, + removedClientIds: [ action.clientId ], addedBlocks: [ updatedBlock ], isNavMode: true, } ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index ed9e859f028a98..31ee6778da8d0d 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -128,7 +128,7 @@ export function isBlockValid( state, clientId ) { * @param {Object} state Editor state. * @param {string} clientId Block client ID. * - * @return {Object?} Block attributes. + * @return {?Object} Block attributes. */ export function getBlockAttributes( state, clientId ) { const block = state.blocks.byClientId.get( clientId ); @@ -1992,7 +1992,7 @@ const getItemFromVariation = ( state, item ) => ( variation ) => { * Returns the calculated frecency. * * 'frecency' is a heuristic (https://en.wikipedia.org/wiki/Frecency) - * that combines block usage frequenty and recency. + * that combines block usage frequency and recency. * * @param {number} time When the last insert occurred as a UNIX epoch * @param {number} count The number of inserts that have occurred. @@ -2080,7 +2080,7 @@ const buildBlockTypeItem = * inserter and handle its selection. * * The 'frecency' property is a heuristic (https://en.wikipedia.org/wiki/Frecency) - * that combines block usage frequenty and recency. + * that combines block usage frequency and recency. * * Items are returned ordered descendingly by their 'utility' and 'frecency'. * @@ -2236,7 +2236,7 @@ export const getInserterItems = createRegistrySelector( ( select ) => * transform list and handle its selection. * * The 'frecency' property is a heuristic (https://en.wikipedia.org/wiki/Frecency) - * that combines block usage frequenty and recency. + * that combines block usage frequency and recency. * * Items are returned ordered descendingly by their 'frecency'. * @@ -2400,7 +2400,7 @@ export const __experimentalGetAllowedBlocks = createSelector( * @typedef {Object} WPDirectInsertBlock * @property {string} name The type of block. * @property {?Object} attributes Attributes to pass to the newly created block. - * @property {?Array<string>} attributesToCopy Attributes to be copied from adjecent blocks when inserted. + * @property {?Array<string>} attributesToCopy Attributes to be copied from adjacent blocks when inserted. */ export function getDirectInsertBlock( state, rootClientId = null ) { if ( ! rootClientId ) { @@ -3087,9 +3087,7 @@ export const getBlockEditingMode = createRegistrySelector( const isContent = hasContentRoleAttribute( name ); return isContent ? 'contentOnly' : 'disabled'; } - // Otherwise, check if there's an ancestor that is contentOnly - const parentMode = getBlockEditingMode( state, rootClientId ); - return parentMode === 'contentOnly' ? 'default' : parentMode; + return 'default'; } ); diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index 268d463f227d4d..07c133dbacafe3 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -122,6 +122,7 @@ describe( 'private selectors', () => { '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': {}, }, blockEditingModes: new Map( [] ), + derivedBlockEditingModes: new Map( [] ), }; const hasContentRoleAttribute = jest.fn( () => false ); @@ -142,6 +143,7 @@ describe( 'private selectors', () => { const state = { ...baseState, blockEditingModes: new Map( [] ), + derivedBlockEditingModes: new Map( [] ), }; expect( isBlockSubtreeDisabled( @@ -157,6 +159,12 @@ describe( 'private selectors', () => { blockEditingModes: new Map( [ [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], ] ), + derivedBlockEditingModes: new Map( [ + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], + ] ), }; expect( isBlockSubtreeDisabled( @@ -166,10 +174,18 @@ describe( 'private selectors', () => { ).toBe( true ); } ); - it( 'should return true when top level block is disabled via inheritence and there are no editing modes within it', () => { + it( 'should return true when top level block is disabled via inheritance and there are no editing modes within it', () => { const state = { ...baseState, blockEditingModes: new Map( [ [ '', 'disabled' ] ] ), + derivedBlockEditingModes: new Map( [ + [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ], + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], + ] ), }; expect( isBlockSubtreeDisabled( @@ -186,6 +202,11 @@ describe( 'private selectors', () => { [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], ] ), + derivedBlockEditingModes: new Map( [ + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], + ] ), }; expect( isBlockSubtreeDisabled( @@ -202,6 +223,11 @@ describe( 'private selectors', () => { [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'default' ], ] ), + derivedBlockEditingModes: new Map( [ + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], + ] ), }; expect( isBlockSubtreeDisabled( @@ -218,6 +244,13 @@ describe( 'private selectors', () => { [ '', 'disabled' ], [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'default' ], ] ), + derivedBlockEditingModes: new Map( [ + [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ], + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], + ] ), }; expect( isBlockSubtreeDisabled( @@ -303,6 +336,7 @@ describe( 'private selectors', () => { const state = { ...baseState, blockEditingModes: new Map( [] ), + derivedBlockEditingModes: new Map( [] ), }; expect( getEnabledClientIdsTree( state ) ).toEqual( [ { @@ -340,6 +374,7 @@ describe( 'private selectors', () => { const state = { ...baseState, blockEditingModes: new Map( [] ), + derivedBlockEditingModes: new Map( [] ), }; expect( getEnabledClientIdsTree( @@ -375,6 +410,10 @@ describe( 'private selectors', () => { [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'contentOnly' ], [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'contentOnly' ], ] ), + derivedBlockEditingModes: new Map( [ + [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ], + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], + ] ), }; expect( getEnabledClientIdsTree( state ) ).toEqual( [ { @@ -412,6 +451,7 @@ describe( 'private selectors', () => { ] ), }, blockEditingModes: new Map(), + derivedBlockEditingModes: new Map(), }; expect( getEnabledBlockParents( @@ -433,7 +473,7 @@ describe( 'private selectors', () => { ], [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', + 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', ], [ '4c2b7140-fffd-44b4-b2a7-820c670a6514', @@ -442,6 +482,7 @@ describe( 'private selectors', () => { ] ), order: new Map( [ + [ '', [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' ] ], [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', [ @@ -453,12 +494,15 @@ describe( 'private selectors', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', [ '4c2b7140-fffd-44b4-b2a7-820c670a6514' ], ], - [ '', [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' ] ], ] ), }, blockEditingModes: new Map( [ [ '', 'disabled' ], - [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'default' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'default' ], + ] ), + derivedBlockEditingModes: new Map( [ + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], ] ), blockListSettings: {}, }; @@ -467,10 +511,7 @@ describe( 'private selectors', () => { state, '4c2b7140-fffd-44b4-b2a7-820c670a6514' ) - ).toEqual( [ - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', - ] ); + ).toEqual( [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c' ] ); } ); it( 'should order from bottom to top if ascending is true', () => { @@ -493,6 +534,7 @@ describe( 'private selectors', () => { ], ] ), order: new Map( [ + [ '', [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' ] ], [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f' ], @@ -505,13 +547,15 @@ describe( 'private selectors', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', [ '4c2b7140-fffd-44b4-b2a7-820c670a6514' ], ], - [ '', [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' ] ], ] ), }, blockEditingModes: new Map( [ [ '', 'disabled' ], [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'default' ], ] ), + derivedBlockEditingModes: new Map( [ + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], + ] ), blockListSettings: {}, }; expect( diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index dd1665d6736ada..6706ff2fbb59e2 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -12,8 +12,7 @@ import { createBlock, privateApis, } from '@wordpress/blocks'; -import { combineReducers, select } from '@wordpress/data'; -import { store as preferencesStore } from '@wordpress/preferences'; +import { combineReducers } from '@wordpress/data'; /** * Internal dependencies @@ -3576,6 +3575,7 @@ describe( 'state', () => { blocks, settings, zoomLevel, + blockEditingModes, } ) ); @@ -3598,15 +3598,6 @@ describe( 'state', () => { describe( 'edit mode', () => { let initialState; beforeAll( () => { - select.mockImplementation( ( storeName ) => { - if ( storeName === preferencesStore ) { - return { - get: jest.fn( () => 'edit' ), - }; - } - return select( storeName ); - } ); - initialState = dispatchActions( [ { @@ -3651,10 +3642,6 @@ describe( 'state', () => { ); } ); - afterAll( () => { - select.mockRestore(); - } ); - it( 'returns no block editing modes when zoomed out / navigation mode are not active and there are no synced patterns', () => { expect( initialState.derivedBlockEditingModes ).toEqual( new Map() @@ -3665,15 +3652,6 @@ describe( 'state', () => { describe( 'synced patterns', () => { let initialState; beforeAll( () => { - select.mockImplementation( ( storeName ) => { - if ( storeName === preferencesStore ) { - return { - get: jest.fn( () => 'edit' ), - }; - } - return select( storeName ); - } ); - // Simulates how the editor typically inserts controlled blocks, // - first the pattern is inserted with no inner blocks. // - next the pattern is marked as a controlled block. @@ -3818,10 +3796,6 @@ describe( 'state', () => { ); } ); - afterAll( () => { - select.mockRestore(); - } ); - it( 'returns the expected block editing modes for synced patterns', () => { // Only the parent pattern and its own children that have bindings // are in contentOnly mode. All other blocks are disabled. @@ -3840,60 +3814,8 @@ describe( 'state', () => { ); } ); - it( 'removes block editing modes when synced patterns are removed', () => { - const { derivedBlockEditingModes } = dispatchActions( - [ - { - type: 'REMOVE_BLOCKS', - clientIds: [ 'root-pattern' ], - }, - ], - testReducer, - initialState - ); - - expect( derivedBlockEditingModes ).toEqual( new Map() ); - } ); - - it( 'returns the expected block editing modes for synced patterns when switching to navigation mode', () => { - select.mockImplementation( ( storeName ) => { - if ( storeName === preferencesStore ) { - return { - get: jest.fn( () => 'navigation' ), - }; - } - return select( storeName ); - } ); - - const { - derivedBlockEditingModes, - derivedNavModeBlockEditingModes, - } = dispatchActions( - [ - { - type: 'SET_EDITOR_MODE', - mode: 'navigation', - }, - ], - testReducer, - initialState - ); - - expect( derivedBlockEditingModes ).toEqual( - new Map( - Object.entries( { - 'pattern-paragraph': 'disabled', - 'pattern-group': 'disabled', - 'pattern-paragraph-with-overrides': 'contentOnly', // Pattern child with bindings. - 'nested-pattern': 'disabled', - 'nested-paragraph': 'disabled', - 'nested-group': 'disabled', - 'nested-paragraph-with-overrides': 'disabled', - } ) - ) - ); - - expect( derivedNavModeBlockEditingModes ).toEqual( + it( 'returns the expected block editing modes for synced patterns in navigation mode', () => { + expect( initialState.derivedNavModeBlockEditingModes ).toEqual( new Map( Object.entries( { '': 'contentOnly', // Section root. @@ -3912,15 +3834,21 @@ describe( 'state', () => { } ) ) ); + } ); - select.mockImplementation( ( storeName ) => { - if ( storeName === preferencesStore ) { - return { - get: jest.fn( () => 'edit' ), - }; - } - return select( storeName ); - } ); + it( 'removes block editing modes when synced patterns are removed', () => { + const { derivedBlockEditingModes } = dispatchActions( + [ + { + type: 'REMOVE_BLOCKS', + clientIds: [ 'root-pattern' ], + }, + ], + testReducer, + initialState + ); + + expect( derivedBlockEditingModes ).toEqual( new Map() ); } ); it( 'returns the expected block editing modes for synced patterns when switching to zoomed out mode', () => { @@ -3961,52 +3889,104 @@ describe( 'state', () => { let initialState; beforeAll( () => { - select.mockImplementation( ( storeName ) => { - if ( storeName === preferencesStore ) { - return { - get: jest.fn( () => 'navigation' ), - }; - } - return select( storeName ); - } ); - initialState = dispatchActions( [ { type: 'UPDATE_SETTINGS', settings: { - [ sectionRootClientIdKey ]: '', + [ sectionRootClientIdKey ]: 'section-root', }, }, { type: 'RESET_BLOCKS', blocks: [ + { + name: 'core/template-part', + clientId: 'header', + attributes: {}, + innerBlocks: [], + }, { name: 'core/group', - clientId: 'group-1', + clientId: 'section-root', attributes: {}, innerBlocks: [ - { - name: 'core/paragraph', - clientId: 'paragraph-1', - attributes: {}, - innerBlocks: [], - }, { name: 'core/group', - clientId: 'group-2', + clientId: 'group-1', attributes: {}, innerBlocks: [ { name: 'core/paragraph', - clientId: 'paragraph-2', + clientId: 'paragraph-1', attributes: {}, innerBlocks: [], }, + { + name: 'core/group', + clientId: 'group-2', + attributes: {}, + innerBlocks: [ + { + name: 'core/paragraph', + clientId: + 'paragraph-2', + attributes: {}, + innerBlocks: [], + }, + ], + }, ], }, ], }, + { + name: 'core/template-part', + clientId: 'footer', + attributes: {}, + innerBlocks: [], + }, + ], + }, + { + type: 'SET_HAS_CONTROLLED_INNER_BLOCKS', + clientId: 'header', + hasControlledInnerBlocks: true, + }, + { + type: 'REPLACE_INNER_BLOCKS', + rootClientId: 'header', + blocks: [ + { + name: 'core/group', + clientId: 'header-group', + attributes: {}, + innerBlocks: [ + { + name: 'core/paragraph', + clientId: 'header-paragraph', + attributes: {}, + innerBlocks: [], + }, + ], + }, + ], + }, + { + type: 'SET_HAS_CONTROLLED_INNER_BLOCKS', + clientId: 'footer', + hasControlledInnerBlocks: true, + }, + { + type: 'REPLACE_INNER_BLOCKS', + rootClientId: 'footer', + blocks: [ + { + name: 'core/paragraph', + clientId: 'footer-paragraph', + attributes: {}, + innerBlocks: [], + }, ], }, ], @@ -4014,15 +3994,17 @@ describe( 'state', () => { ); } ); - afterAll( () => { - select.mockRestore(); - } ); - it( 'returns the expected block editing modes', () => { expect( initialState.derivedNavModeBlockEditingModes ).toEqual( new Map( Object.entries( { - '': 'contentOnly', // Section root. + '': 'disabled', + header: 'contentOnly', // Template part. + 'header-group': 'disabled', // Content block in template part. + 'header-paragraph': 'contentOnly', // Content block in template part. + footer: 'contentOnly', // Template part. + 'footer-paragraph': 'contentOnly', // Content block in template part. + 'section-root': 'contentOnly', // Section root. 'group-1': 'contentOnly', // Section block. 'paragraph-1': 'contentOnly', // Content block in section. 'group-2': 'disabled', // Non-content block in section. @@ -4032,6 +4014,49 @@ describe( 'state', () => { ); } ); + it( 'allows content blocks to be disabled explicitly using the block editing mode', () => { + const { + derivedNavModeBlockEditingModes, + blockEditingModes: _blockEditingModes, + } = dispatchActions( + [ + { + type: 'SET_BLOCK_EDITING_MODE', + clientId: 'paragraph-1', + mode: 'disabled', + }, + ], + testReducer, + initialState + ); + + // Paragraph 1 is explicitly disabled and omitted from the + // derived block editing modes. + expect( _blockEditingModes ).toEqual( + new Map( + Object.entries( { + 'paragraph-1': 'disabled', + } ) + ) + ); + expect( derivedNavModeBlockEditingModes ).toEqual( + new Map( + Object.entries( { + '': 'disabled', + header: 'contentOnly', + 'header-group': 'disabled', + 'header-paragraph': 'contentOnly', + footer: 'contentOnly', + 'footer-paragraph': 'contentOnly', + 'section-root': 'contentOnly', + 'group-1': 'contentOnly', + 'group-2': 'disabled', + 'paragraph-2': 'contentOnly', + } ) + ) + ); + } ); + it( 'removes block editing modes when blocks are removed', () => { const { derivedNavModeBlockEditingModes } = dispatchActions( [ @@ -4047,7 +4072,13 @@ describe( 'state', () => { expect( derivedNavModeBlockEditingModes ).toEqual( new Map( Object.entries( { - '': 'contentOnly', + '': 'disabled', + header: 'contentOnly', // Template part. + 'header-group': 'disabled', // Content block in template part. + 'header-paragraph': 'contentOnly', // Content block in template part. + footer: 'contentOnly', // Template part. + 'footer-paragraph': 'contentOnly', // Content block in template part. + 'section-root': 'contentOnly', 'group-1': 'contentOnly', 'paragraph-1': 'contentOnly', } ) @@ -4060,7 +4091,7 @@ describe( 'state', () => { [ { type: 'INSERT_BLOCKS', - rootClientId: '', + rootClientId: 'section-root', blocks: [ { name: 'core/group', @@ -4091,7 +4122,13 @@ describe( 'state', () => { expect( derivedNavModeBlockEditingModes ).toEqual( new Map( Object.entries( { - '': 'contentOnly', // Section root. + '': 'disabled', // Section root. + header: 'contentOnly', // Template part. + 'header-group': 'disabled', // Content block in template part. + 'header-paragraph': 'contentOnly', // Content block in template part. + footer: 'contentOnly', // Template part. + 'footer-paragraph': 'contentOnly', // Content block in template part. + 'section-root': 'contentOnly', // Section root. 'group-1': 'contentOnly', // Section block. 'paragraph-1': 'contentOnly', // Content block in section. 'group-2': 'disabled', // Non-content block in section. @@ -4111,7 +4148,7 @@ describe( 'state', () => { type: 'MOVE_BLOCKS_TO_POSITION', clientIds: [ 'group-2' ], fromRootClientId: 'group-1', - toRootClientId: '', + toRootClientId: 'section-root', }, ], testReducer, @@ -4120,7 +4157,13 @@ describe( 'state', () => { expect( derivedNavModeBlockEditingModes ).toEqual( new Map( Object.entries( { - '': 'contentOnly', // Section root. + '': 'disabled', // Section root. + header: 'contentOnly', // Template part. + 'header-group': 'disabled', // Content block in template part. + 'header-paragraph': 'contentOnly', // Content block in template part. + footer: 'contentOnly', // Template part. + 'footer-paragraph': 'contentOnly', // Content block in template part. + 'section-root': 'contentOnly', // Section root. 'group-1': 'contentOnly', // Section block. 'paragraph-1': 'contentOnly', // Content block in section. 'group-2': 'contentOnly', // New section block. @@ -4148,10 +4191,16 @@ describe( 'state', () => { new Map( Object.entries( { '': 'disabled', - 'group-1': 'contentOnly', - 'paragraph-1': 'contentOnly', - 'group-2': 'contentOnly', - 'paragraph-2': 'contentOnly', + header: 'contentOnly', // Template part. + 'header-group': 'disabled', // Content block in template part. + 'header-paragraph': 'contentOnly', // Content block in template part. + footer: 'contentOnly', // Template part. + 'footer-paragraph': 'contentOnly', // Content block in template part. + 'section-root': 'disabled', + 'group-1': 'contentOnly', // New section root. + 'paragraph-1': 'contentOnly', // Section and content block + 'group-2': 'contentOnly', // Section. + 'paragraph-2': 'contentOnly', // Content block. } ) ) ); @@ -4224,49 +4273,6 @@ describe( 'state', () => { ); } ); - it( 'overrides navigation mode', () => { - select.mockImplementation( ( storeName ) => { - if ( storeName === preferencesStore ) { - return { - get: jest.fn( () => 'navigation' ), - }; - } - return select( storeName ); - } ); - - const { derivedBlockEditingModes } = dispatchActions( - [ - { - type: 'SET_EDITOR_MODE', - mode: 'navigation', - }, - ], - testReducer, - initialState - ); - - expect( derivedBlockEditingModes ).toEqual( - new Map( - Object.entries( { - '': 'contentOnly', // Section root. - 'group-1': 'contentOnly', // Section block. - 'paragraph-1': 'disabled', - 'group-2': 'disabled', - 'paragraph-2': 'disabled', - } ) - ) - ); - - select.mockImplementation( ( storeName ) => { - if ( storeName === preferencesStore ) { - return { - get: jest.fn( () => 'edit' ), - }; - } - return select( storeName ); - } ); - } ); - it( 'removes block editing modes when blocks are removed', () => { const { derivedBlockEditingModes } = dispatchActions( [ diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 51949bfd468ca8..388d592787b660 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -4465,6 +4465,7 @@ describe( 'getBlockEditingMode', () => { '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f': {}, }, blockEditingModes: new Map( [] ), + derivedBlockEditingModes: new Map( [] ), }; const hasContentRoleAttribute = jest.fn( () => false ); @@ -4519,6 +4520,13 @@ describe( 'getBlockEditingMode', () => { blockEditingModes: new Map( [ [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], ] ), + derivedBlockEditingModes: new Map( [ + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ], + ] ), }; expect( getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) @@ -4545,6 +4553,12 @@ describe( 'getBlockEditingMode', () => { [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'default' ], [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], ] ), + derivedBlockEditingModes: new Map( [ + [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ], + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ], + ] ), }; expect( getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) @@ -4555,6 +4569,15 @@ describe( 'getBlockEditingMode', () => { const state = { ...baseState, blockEditingModes: new Map( [ [ '', 'disabled' ] ] ), + derivedBlockEditingModes: new Map( [ + [ '6cf70164-9097-4460-bcbf-200560546988', 'disabled' ], + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', 'disabled' ], + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', 'disabled' ], + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'disabled' ], + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', 'disabled' ], + [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', 'disabled' ], + ] ), }; expect( getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) diff --git a/packages/block-editor/src/utils/block-bindings.js b/packages/block-editor/src/utils/block-bindings.js index dcf80d985473b2..9a4c6acf9a9032 100644 --- a/packages/block-editor/src/utils/block-bindings.js +++ b/packages/block-editor/src/utils/block-bindings.js @@ -9,10 +9,105 @@ import { useDispatch, useRegistry } from '@wordpress/data'; import { store as blockEditorStore } from '../store'; import { useBlockEditContext } from '../components/block-edit'; +const DEFAULT_ATTRIBUTE = '__default'; +const PATTERN_OVERRIDES_SOURCE = 'core/pattern-overrides'; +const BLOCK_BINDINGS_ALLOWED_BLOCKS = { + 'core/paragraph': [ 'content' ], + 'core/heading': [ 'content' ], + 'core/image': [ 'id', 'url', 'title', 'alt' ], + 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], +}; + +/** + * Checks if the given object is empty. + * + * @param {?Object} object The object to check. + * + * @return {boolean} Whether the object is empty. + */ function isObjectEmpty( object ) { return ! object || Object.keys( object ).length === 0; } +/** + * Based on the given block name, checks if it is possible to bind the block. + * + * @param {string} blockName The name of the block. + * + * @return {boolean} Whether it is possible to bind the block to sources. + */ +export function canBindBlock( blockName ) { + return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; +} + +/** + * Based on the given block name and attribute name, checks if it is possible to bind the block attribute. + * + * @param {string} blockName The name of the block. + * @param {string} attributeName The name of attribute. + * + * @return {boolean} Whether it is possible to bind the block attribute. + */ +export function canBindAttribute( blockName, attributeName ) { + return ( + canBindBlock( blockName ) && + BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName ) + ); +} + +/** + * Gets the bindable attributes for a given block. + * + * @param {string} blockName The name of the block. + * + * @return {string[]} The bindable attributes for the block. + */ +export function getBindableAttributes( blockName ) { + return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; +} + +/** + * Checks if the block has the `__default` binding for pattern overrides. + * + * @param {?Record<string, object>} bindings A block's bindings from the metadata attribute. + * + * @return {boolean} Whether the block has the `__default` binding for pattern overrides. + */ +export function hasPatternOverridesDefaultBinding( bindings ) { + return bindings?.[ DEFAULT_ATTRIBUTE ]?.source === PATTERN_OVERRIDES_SOURCE; +} + +/** + * Returns the bindings with the `__default` binding for pattern overrides + * replaced with the full-set of supported attributes. e.g.: + * + * - bindings passed in: `{ __default: { source: 'core/pattern-overrides' } }` + * - bindings returned: `{ content: { source: 'core/pattern-overrides' } }` + * + * @param {string} blockName The block name (e.g. 'core/paragraph'). + * @param {?Record<string, object>} bindings A block's bindings from the metadata attribute. + * + * @return {Object} The bindings with default replaced for pattern overrides. + */ +export function replacePatternOverridesDefaultBinding( blockName, bindings ) { + // The `__default` binding currently only works for pattern overrides. + if ( hasPatternOverridesDefaultBinding( bindings ) ) { + const supportedAttributes = BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; + const bindingsWithDefaults = {}; + for ( const attributeName of supportedAttributes ) { + // If the block has mixed binding sources, retain any non pattern override bindings. + const bindingSource = bindings[ attributeName ] + ? bindings[ attributeName ] + : { source: PATTERN_OVERRIDES_SOURCE }; + bindingsWithDefaults[ attributeName ] = bindingSource; + } + + return bindingsWithDefaults; + } + + return bindings; +} + /** * Contains utils to update the block `bindings` metadata. * diff --git a/packages/block-editor/tsconfig.json b/packages/block-editor/tsconfig.json index a3c7d1ffd88077..a3a6bd18f451d5 100644 --- a/packages/block-editor/tsconfig.json +++ b/packages/block-editor/tsconfig.json @@ -1,10 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, "references": [ { "path": "../a11y" }, { "path": "../api-fetch" }, @@ -37,5 +33,6 @@ // NOTE: This package is being progressively typed. You are encouraged to // expand this array with files which can be type-checked. At some point in // the future, this can be simplified to an `includes` of `src/**/*`. - "files": [ "src/components/block-context/index.js", "src/utils/dom.js" ] + "files": [ "src/components/block-context/index.js", "src/utils/dom.js" ], + "include": [] } diff --git a/packages/block-library/src/archives/edit.js b/packages/block-library/src/archives/edit.js index 60b8715988ed94..d4f25da8507f3e 100644 --- a/packages/block-library/src/archives/edit.js +++ b/packages/block-library/src/archives/edit.js @@ -2,70 +2,128 @@ * WordPress dependencies */ import { - PanelBody, ToggleControl, SelectControl, Disabled, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; import ServerSideRender from '@wordpress/server-side-render'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + export default function ArchivesEdit( { attributes, setAttributes } ) { const { showLabel, showPostCounts, displayAsDropdown, type } = attributes; + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + return ( <> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + displayAsDropdown: false, + showLabel: false, + showPostCounts: false, + type: 'monthly', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem label={ __( 'Display as dropdown' ) } - checked={ displayAsDropdown } - onChange={ () => - setAttributes( { - displayAsDropdown: ! displayAsDropdown, - } ) + isShownByDefault + hasValue={ () => displayAsDropdown } + onDeselect={ () => + setAttributes( { displayAsDropdown: false } ) } - /> - { displayAsDropdown && ( + > <ToggleControl __nextHasNoMarginBottom - label={ __( 'Show label' ) } - checked={ showLabel } + label={ __( 'Display as dropdown' ) } + checked={ displayAsDropdown } onChange={ () => setAttributes( { - showLabel: ! showLabel, + displayAsDropdown: ! displayAsDropdown, } ) } /> + </ToolsPanelItem> + + { displayAsDropdown && ( + <ToolsPanelItem + label={ __( 'Show label' ) } + isShownByDefault + hasValue={ () => showLabel } + onDeselect={ () => + setAttributes( { showLabel: false } ) + } + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Show label' ) } + checked={ showLabel } + onChange={ () => + setAttributes( { + showLabel: ! showLabel, + } ) + } + /> + </ToolsPanelItem> ) } - <ToggleControl - __nextHasNoMarginBottom + + <ToolsPanelItem label={ __( 'Show post counts' ) } - checked={ showPostCounts } - onChange={ () => - setAttributes( { - showPostCounts: ! showPostCounts, - } ) + isShownByDefault + hasValue={ () => showPostCounts } + onDeselect={ () => + setAttributes( { showPostCounts: false } ) } - /> - <SelectControl - __next40pxDefaultSize - __nextHasNoMarginBottom + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Show post counts' ) } + checked={ showPostCounts } + onChange={ () => + setAttributes( { + showPostCounts: ! showPostCounts, + } ) + } + /> + </ToolsPanelItem> + + <ToolsPanelItem label={ __( 'Group by' ) } - options={ [ - { label: __( 'Year' ), value: 'yearly' }, - { label: __( 'Month' ), value: 'monthly' }, - { label: __( 'Week' ), value: 'weekly' }, - { label: __( 'Day' ), value: 'daily' }, - ] } - value={ type } - onChange={ ( value ) => - setAttributes( { type: value } ) + isShownByDefault + hasValue={ () => !! type } + onDeselect={ () => + setAttributes( { type: 'monthly' } ) } - /> - </PanelBody> + > + <SelectControl + __next40pxDefaultSize + __nextHasNoMarginBottom + label={ __( 'Group by' ) } + options={ [ + { label: __( 'Year' ), value: 'yearly' }, + { label: __( 'Month' ), value: 'monthly' }, + { label: __( 'Week' ), value: 'weekly' }, + { label: __( 'Day' ), value: 'daily' }, + ] } + value={ type } + onChange={ ( value ) => + setAttributes( { type: value } ) + } + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <div { ...useBlockProps() }> <Disabled> 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 4cf28f7063ad31..9cf88d804068af 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 @@ -89,7 +89,7 @@ exports[`Audio block renders audio block error state without crashing 1`] = ` <Svg height={16} style={{}} - viewBox="-2 -2 24 24" + viewBox="0 0 24 24" width={16} xmlns="http://www.w3.org/2000/svg" > diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 8beef975fad6f3..e8075115cabda4 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -87,6 +87,26 @@ function render_block_core_block( $attributes ) { add_filter( 'render_block_context', $filter_block_context, 1 ); } + $ignored_hooked_blocks = get_post_meta( $attributes['ref'], '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $ignored_hooked_blocks ) ) { + $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); + $attributes['metadata'] = array( + 'ignoredHookedBlocks' => $ignored_hooked_blocks, + ); + } + + // Wrap in "Block" block so the Block Hooks algorithm can insert blocks + // that are hooked as first or last child of `core/block`. + $content = get_comment_delimited_block_content( + 'core/block', + $attributes, + $content + ); + // Apply Block Hooks. + $content = apply_block_hooks_to_content( $content, $reusable_block ); + // Remove block wrapper. + $content = remove_serialized_parent_block( $content ); + $content = do_blocks( $content ); unset( $seen_refs[ $attributes['ref'] ] ); diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index 2c1c05baa20dd3..6fcb7aca4c5923 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -85,6 +85,16 @@ } }, "typography": { + "__experimentalSkipSerialization": [ + "fontSize", + "lineHeight", + "fontFamily", + "fontWeight", + "fontStyle", + "textTransform", + "textDecoration", + "letterSpacing" + ], "fontSize": true, "lineHeight": true, "__experimentalFontFamily": true, @@ -122,7 +132,6 @@ "width": true } }, - "__experimentalSelector": ".wp-block-button .wp-block-button__link", "interactivity": { "clientNavigation": true } @@ -132,5 +141,11 @@ { "name": "outline", "label": "Outline" } ], "editorStyle": "wp-block-button-editor", - "style": "wp-block-button" + "style": "wp-block-button", + "selectors": { + "root": ".wp-block-button .wp-block-button__link", + "typography": { + "writingMode": ".wp-block-button" + } + } } diff --git a/packages/block-library/src/button/deprecated.js b/packages/block-library/src/button/deprecated.js index 8ab83e1b09518f..f478c39a0dc326 100644 --- a/packages/block-library/src/button/deprecated.js +++ b/packages/block-library/src/button/deprecated.js @@ -14,6 +14,8 @@ import { __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles, + __experimentalGetShadowClassesAndStyles as getShadowClassesAndStyles, + __experimentalGetElementClassName, } from '@wordpress/block-editor'; import { compose } from '@wordpress/compose'; @@ -132,6 +134,192 @@ const blockAttributes = { }, }; +const v12 = { + attributes: { + tagName: { + type: 'string', + enum: [ 'a', 'button' ], + default: 'a', + }, + type: { + type: 'string', + default: 'button', + }, + textAlign: { + type: 'string', + }, + url: { + type: 'string', + source: 'attribute', + selector: 'a', + attribute: 'href', + }, + title: { + type: 'string', + source: 'attribute', + selector: 'a,button', + attribute: 'title', + role: 'content', + }, + text: { + type: 'rich-text', + source: 'rich-text', + selector: 'a,button', + role: 'content', + }, + linkTarget: { + type: 'string', + source: 'attribute', + selector: 'a', + attribute: 'target', + role: 'content', + }, + rel: { + type: 'string', + source: 'attribute', + selector: 'a', + attribute: 'rel', + role: 'content', + }, + placeholder: { + type: 'string', + }, + backgroundColor: { + type: 'string', + }, + textColor: { + type: 'string', + }, + gradient: { + type: 'string', + }, + width: { + type: 'number', + }, + }, + supports: { + anchor: true, + align: true, + alignWide: false, + color: { + __experimentalSkipSerialization: true, + gradients: true, + __experimentalDefaultControls: { + background: true, + text: true, + }, + }, + typography: { + fontSize: true, + lineHeight: true, + __experimentalFontFamily: true, + __experimentalFontWeight: true, + __experimentalFontStyle: true, + __experimentalTextTransform: true, + __experimentalTextDecoration: true, + __experimentalLetterSpacing: true, + __experimentalWritingMode: true, + __experimentalDefaultControls: { + fontSize: true, + }, + }, + reusable: false, + shadow: { + __experimentalSkipSerialization: true, + }, + spacing: { + __experimentalSkipSerialization: true, + padding: [ 'horizontal', 'vertical' ], + __experimentalDefaultControls: { + padding: true, + }, + }, + __experimentalBorder: { + color: true, + radius: true, + style: true, + width: true, + __experimentalSkipSerialization: true, + __experimentalDefaultControls: { + color: true, + radius: true, + style: true, + width: true, + }, + }, + __experimentalSelector: '.wp-block-button__link', + interactivity: { + clientNavigation: true, + }, + }, + save( { attributes, className } ) { + const { + tagName, + type, + textAlign, + fontSize, + linkTarget, + rel, + style, + text, + title, + url, + width, + } = attributes; + + const TagName = tagName || 'a'; + const isButtonTag = 'button' === TagName; + const buttonType = type || 'button'; + const borderProps = getBorderClassesAndStyles( attributes ); + const colorProps = getColorClassesAndStyles( attributes ); + const spacingProps = getSpacingClassesAndStyles( attributes ); + const shadowProps = getShadowClassesAndStyles( attributes ); + const buttonClasses = clsx( + 'wp-block-button__link', + colorProps.className, + borderProps.className, + { + [ `has-text-align-${ textAlign }` ]: textAlign, + // For backwards compatibility add style that isn't provided via + // block support. + 'no-border-radius': style?.border?.radius === 0, + }, + __experimentalGetElementClassName( 'button' ) + ); + const buttonStyle = { + ...borderProps.style, + ...colorProps.style, + ...spacingProps.style, + ...shadowProps.style, + }; + + // The use of a `title` attribute here is soft-deprecated, but still applied + // if it had already been assigned, for the sake of backward-compatibility. + // A title will no longer be assigned for new or updated button block links. + + const wrapperClasses = clsx( className, { + [ `has-custom-width wp-block-button__width-${ width }` ]: width, + [ `has-custom-font-size` ]: fontSize || style?.typography?.fontSize, + } ); + + return ( + <div { ...useBlockProps.save( { className: wrapperClasses } ) }> + <RichText.Content + tagName={ TagName } + type={ isButtonTag ? buttonType : null } + className={ buttonClasses } + href={ isButtonTag ? null : url } + title={ title } + style={ buttonStyle } + value={ text } + target={ isButtonTag ? null : linkTarget } + rel={ isButtonTag ? null : rel } + /> + </div> + ); + }, +}; + const v11 = { attributes: { url: { @@ -399,6 +587,7 @@ const v10 = { }; const deprecated = [ + v12, v11, v10, { diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 2106c2031491fe..593066d6555b40 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -9,6 +9,7 @@ import clsx from 'clsx'; import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants'; import { getUpdatedLinkAttributes } from './get-updated-link-attributes'; import removeAnchorTag from '../utils/remove-anchor-tag'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; /** * WordPress dependencies @@ -16,12 +17,13 @@ import removeAnchorTag from '../utils/remove-anchor-tag'; import { __ } from '@wordpress/i18n'; import { useEffect, useState, useRef, useMemo } from '@wordpress/element'; import { - Button, - ButtonGroup, - PanelBody, TextControl, ToolbarButton, Popover, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, } from '@wordpress/components'; import { AlignmentControl, @@ -37,6 +39,8 @@ import { __experimentalGetElementClassName, store as blockEditorStore, useBlockEditingMode, + getTypographyClassesAndStyles as useTypographyProps, + useSettings, } from '@wordpress/block-editor'; import { displayShortcut, isKeyboardEvent, ENTER } from '@wordpress/keycodes'; import { link, linkOff } from '@wordpress/icons'; @@ -114,35 +118,43 @@ function useEnter( props ) { } function WidthPanel( { selectedWidth, setAttributes } ) { - function handleChange( newWidth ) { - // Check if we are toggling the width off - const width = selectedWidth === newWidth ? undefined : newWidth; - - // Update attributes. - setAttributes( { width } ); - } + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); return ( - <PanelBody title={ __( 'Settings' ) }> - <ButtonGroup aria-label={ __( 'Button width' ) }> - { [ 25, 50, 75, 100 ].map( ( widthValue ) => { - return ( - <Button - key={ widthValue } - size="small" - variant={ - widthValue === selectedWidth - ? 'primary' - : undefined - } - onClick={ () => handleChange( widthValue ) } - > - { widthValue }% - </Button> - ); - } ) } - </ButtonGroup> - </PanelBody> + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => setAttributes( { width: undefined } ) } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + label={ __( 'Width' ) } + isShownByDefault + hasValue={ () => !! selectedWidth } + onDeselect={ () => setAttributes( { width: undefined } ) } + __nextHasNoMarginBottom + > + <ToggleGroupControl + label={ __( 'Width' ) } + value={ selectedWidth } + onChange={ ( newWidth ) => + setAttributes( { width: newWidth } ) + } + isBlock + __next40pxDefaultSize + __nextHasNoMarginBottom + > + { [ 25, 50, 75, 100 ].map( ( widthValue ) => { + return ( + <ToggleGroupControlOption + key={ widthValue } + value={ widthValue } + label={ `${ widthValue }%` } + /> + ); + } ) } + </ToggleGroupControl> + </ToolsPanelItem> + </ToolsPanel> ); } @@ -256,6 +268,19 @@ function ButtonEdit( props ) { [ context, isSelected, metadata?.bindings?.url ] ); + const [ fluidTypographySettings, layout ] = useSettings( + 'typography.fluid', + 'layout' + ); + const typographyProps = useTypographyProps( attributes, { + typography: { + fluid: fluidTypographySettings, + }, + layout: { + wideSize: layout?.wideSize, + }, + } ); + return ( <> <div @@ -263,7 +288,6 @@ function ButtonEdit( props ) { className={ clsx( blockProps.className, { [ `has-custom-width wp-block-button__width-${ width }` ]: width, - [ `has-custom-font-size` ]: blockProps.style.fontSize, } ) } > <RichText @@ -282,11 +306,14 @@ function ButtonEdit( props ) { 'wp-block-button__link', colorProps.className, borderProps.className, + typographyProps.className, { [ `has-text-align-${ textAlign }` ]: textAlign, // For backwards compatibility add style that isn't // provided via block support. 'no-border-radius': style?.border?.radius === 0, + [ `has-custom-font-size` ]: + blockProps.style.fontSize, }, __experimentalGetElementClassName( 'button' ) ) } @@ -295,6 +322,8 @@ function ButtonEdit( props ) { ...colorProps.style, ...spacingProps.style, ...shadowProps.style, + ...typographyProps.style, + writingMode: undefined, } } onReplace={ onReplace } onMerge={ mergeBlocks } diff --git a/packages/block-library/src/button/save.js b/packages/block-library/src/button/save.js index 8cb9da6fbfbc18..4255868d50fbc5 100644 --- a/packages/block-library/src/button/save.js +++ b/packages/block-library/src/button/save.js @@ -14,6 +14,7 @@ import { __experimentalGetSpacingClassesAndStyles as getSpacingClassesAndStyles, __experimentalGetShadowClassesAndStyles as getShadowClassesAndStyles, __experimentalGetElementClassName, + getTypographyClassesAndStyles, } from '@wordpress/block-editor'; export default function save( { attributes, className } ) { @@ -38,15 +39,18 @@ export default function save( { attributes, className } ) { const colorProps = getColorClassesAndStyles( attributes ); const spacingProps = getSpacingClassesAndStyles( attributes ); const shadowProps = getShadowClassesAndStyles( attributes ); + const typographyProps = getTypographyClassesAndStyles( attributes ); const buttonClasses = clsx( 'wp-block-button__link', colorProps.className, borderProps.className, + typographyProps.className, { [ `has-text-align-${ textAlign }` ]: textAlign, // For backwards compatibility add style that isn't provided via // block support. 'no-border-radius': style?.border?.radius === 0, + [ `has-custom-font-size` ]: fontSize || style?.typography?.fontSize, }, __experimentalGetElementClassName( 'button' ) ); @@ -55,6 +59,8 @@ export default function save( { attributes, className } ) { ...colorProps.style, ...spacingProps.style, ...shadowProps.style, + ...typographyProps.style, + writingMode: undefined, }; // The use of a `title` attribute here is soft-deprecated, but still applied @@ -63,7 +69,6 @@ export default function save( { attributes, className } ) { const wrapperClasses = clsx( className, { [ `has-custom-width wp-block-button__width-${ width }` ]: width, - [ `has-custom-font-size` ]: fontSize || style?.typography?.fontSize, } ); return ( diff --git a/packages/block-library/src/column/edit.js b/packages/block-library/src/column/edit.js index a0f3cdcf65393d..92a0b41b44ed52 100644 --- a/packages/block-library/src/column/edit.js +++ b/packages/block-library/src/column/edit.js @@ -18,31 +18,52 @@ import { } from '@wordpress/block-editor'; import { __experimentalUseCustomUnits as useCustomUnits, - PanelBody, __experimentalUnitControl as UnitControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { sprintf, __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + function ColumnInspectorControls( { width, setAttributes } ) { const [ availableUnits ] = useSettings( 'spacing.units' ); const units = useCustomUnits( { availableUnits: availableUnits || [ '%', 'px', 'em', 'rem', 'vw' ], } ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); return ( - <PanelBody title={ __( 'Settings' ) }> - <UnitControl + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { width: undefined } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + hasValue={ () => width !== undefined } label={ __( 'Width' ) } - __unstableInputWidth="calc(50% - 8px)" - __next40pxDefaultSize - value={ width || '' } - onChange={ ( nextWidth ) => { - nextWidth = 0 > parseFloat( nextWidth ) ? '0' : nextWidth; - setAttributes( { width: nextWidth } ); - } } - units={ units } - /> - </PanelBody> + onDeselect={ () => setAttributes( { width: undefined } ) } + isShownByDefault + > + <UnitControl + label={ __( 'Width' ) } + __unstableInputWidth="calc(50% - 8px)" + __next40pxDefaultSize + value={ width || '' } + onChange={ ( nextWidth ) => { + nextWidth = + 0 > parseFloat( nextWidth ) ? '0' : nextWidth; + setAttributes( { width: nextWidth } ); + } } + units={ units } + /> + </ToolsPanelItem> + </ToolsPanel> ); } diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js index f8cf0297302ccd..f454de2e5e1203 100644 --- a/packages/block-library/src/columns/edit.js +++ b/packages/block-library/src/columns/edit.js @@ -9,9 +9,11 @@ import clsx from 'clsx'; import { __ } from '@wordpress/i18n'; import { Notice, - PanelBody, RangeControl, ToggleControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + __experimentalVStack as VStack, } from '@wordpress/components'; import { @@ -39,6 +41,7 @@ import { getRedistributedColumnWidths, toWidthPrecision, } from './utils'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const DEFAULT_BLOCK = { name: 'core/column', @@ -51,19 +54,15 @@ function ColumnInspectorControls( { } ) { const { count, canInsertColumnBlock, minCount } = useSelect( ( select ) => { - const { - canInsertBlockType, - canRemoveBlock, - getBlocks, - getBlockCount, - } = select( blockEditorStore ); - const innerBlocks = getBlocks( clientId ); + const { canInsertBlockType, canRemoveBlock, getBlockOrder } = + select( blockEditorStore ); + const blockOrder = getBlockOrder( clientId ); // Get the indexes of columns for which removal is prevented. // The highest index will be used to determine the minimum column count. - const preventRemovalBlockIndexes = innerBlocks.reduce( - ( acc, block, index ) => { - if ( ! canRemoveBlock( block.clientId ) ) { + const preventRemovalBlockIndexes = blockOrder.reduce( + ( acc, blockId, index ) => { + if ( ! canRemoveBlock( blockId ) ) { acc.push( index ); } return acc; @@ -72,7 +71,7 @@ function ColumnInspectorControls( { ); return { - count: getBlockCount( clientId ), + count: blockOrder.length, canInsertColumnBlock: canInsertBlockType( 'core/column', clientId @@ -148,41 +147,73 @@ function ColumnInspectorControls( { replaceInnerBlocks( clientId, innerBlocks ); } + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + return ( - <PanelBody title={ __( 'Settings' ) }> + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + updateColumns( count, minCount ); + setAttributes( { + isStackedOnMobile: true, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > { canInsertColumnBlock && ( - <> - <RangeControl - __nextHasNoMarginBottom - __next40pxDefaultSize - label={ __( 'Columns' ) } - value={ count } - onChange={ ( value ) => - updateColumns( count, Math.max( minCount, value ) ) - } - min={ Math.max( 1, minCount ) } - max={ Math.max( 6, count ) } - /> - { count > 6 && ( - <Notice status="warning" isDismissible={ false }> - { __( - 'This column count exceeds the recommended amount and may cause visual breakage.' - ) } - </Notice> - ) } - </> + <ToolsPanelItem + label={ __( 'Columns' ) } + isShownByDefault + hasValue={ () => count } + onDeselect={ () => updateColumns( count, minCount ) } + > + <VStack spacing={ 4 }> + <RangeControl + __nextHasNoMarginBottom + __next40pxDefaultSize + label={ __( 'Columns' ) } + value={ count } + onChange={ ( value ) => + updateColumns( + count, + Math.max( minCount, value ) + ) + } + min={ Math.max( 1, minCount ) } + max={ Math.max( 6, count ) } + /> + { count > 6 && ( + <Notice status="warning" isDismissible={ false }> + { __( + 'This column count exceeds the recommended amount and may cause visual breakage.' + ) } + </Notice> + ) } + </VStack> + </ToolsPanelItem> ) } - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanelItem label={ __( 'Stack on mobile' ) } - checked={ isStackedOnMobile } - onChange={ () => + isShownByDefault + hasValue={ () => isStackedOnMobile !== true } + onDeselect={ () => setAttributes( { - isStackedOnMobile: ! isStackedOnMobile, + isStackedOnMobile: true, } ) } - /> - </PanelBody> + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Stack on mobile' ) } + checked={ isStackedOnMobile } + onChange={ () => + setAttributes( { + isStackedOnMobile: ! isStackedOnMobile, + } ) + } + /> + </ToolsPanelItem> + </ToolsPanel> ); } diff --git a/packages/block-library/src/comments/index.js b/packages/block-library/src/comments/index.js index 21db8b986d6e5e..b907bd41e3c6a2 100644 --- a/packages/block-library/src/comments/index.js +++ b/packages/block-library/src/comments/index.js @@ -17,6 +17,7 @@ export { metadata, name }; export const settings = { icon, + example: {}, edit, save, deprecated, diff --git a/packages/block-library/src/cover/edit.native.js b/packages/block-library/src/cover/edit.native.js index 99324545bf798e..7f73ec85a798e6 100644 --- a/packages/block-library/src/cover/edit.native.js +++ b/packages/block-library/src/cover/edit.native.js @@ -58,7 +58,7 @@ import { useCallback, useMemo, } from '@wordpress/element'; -import { cover as icon, replace, image, warning } from '@wordpress/icons'; +import { cover as icon, replace, image, cautionFilled } from '@wordpress/icons'; import { getProtocol } from '@wordpress/url'; // eslint-disable-next-line no-restricted-imports import { store as editPostStore } from '@wordpress/edit-post'; @@ -665,7 +665,10 @@ const Cover = ( { style={ styles.uploadFailedContainer } > <View style={ styles.uploadFailed }> - <Icon icon={ warning } { ...styles.uploadFailedIcon } /> + <Icon + icon={ cautionFilled } + { ...styles.uploadFailedIcon } + /> </View> </View> ) } diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index ced30973203292..1eafe99e283eb4 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -114,11 +114,18 @@ function CoverEdit( { const { __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore ); - const media = useSelect( - ( select ) => - featuredImage && - select( coreStore ).getMedia( featuredImage, { context: 'view' } ), - [ featuredImage ] + const { media } = useSelect( + ( select ) => { + return { + media: + featuredImage && useFeaturedImage + ? select( coreStore ).getMedia( featuredImage, { + context: 'view', + } ) + : undefined, + }; + }, + [ featuredImage, useFeaturedImage ] ); const mediaUrl = media?.media_details?.sizes?.[ sizeSlug ]?.source_url ?? diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js index f5d6a5301ef6d2..72f51150c27443 100644 --- a/packages/block-library/src/cover/test/edit.js +++ b/packages/block-library/src/cover/test/edit.js @@ -200,9 +200,7 @@ describe( 'Cover block', () => { await selectBlock( 'Block: Cover' ); expect( - screen.getByRole( 'heading', { - name: 'Settings', - } ) + await screen.findByRole( 'heading', { name: 'Settings' } ) ).toBeInTheDocument(); } ); } ); @@ -216,7 +214,7 @@ describe( 'Cover block', () => { ); await selectBlock( 'Block: Cover' ); await userEvent.click( - screen.getByLabelText( 'Fixed background' ) + await screen.findByLabelText( 'Fixed background' ) ); expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass( 'has-parallax' @@ -232,7 +230,7 @@ describe( 'Cover block', () => { ); await selectBlock( 'Block: Cover' ); await userEvent.click( - screen.getByLabelText( 'Repeated background' ) + await screen.findByLabelText( 'Repeated background' ) ); expect( screen.getByLabelText( 'Block: Cover' ) ).toHaveClass( 'is-repeated' @@ -245,7 +243,7 @@ describe( 'Cover block', () => { } ); await selectBlock( 'Block: Cover' ); - await userEvent.clear( screen.getByLabelText( 'Left' ) ); + await userEvent.clear( await screen.findByLabelText( 'Left' ) ); await userEvent.type( screen.getByLabelText( 'Left' ), '100' ); expect( @@ -262,7 +260,7 @@ describe( 'Cover block', () => { await selectBlock( 'Block: Cover' ); await userEvent.type( - screen.getByLabelText( 'Alternative text' ), + await screen.findByLabelText( 'Alternative text' ), 'Me' ); expect( screen.getByAltText( 'Me' ) ).toBeInTheDocument(); diff --git a/packages/block-library/src/details/edit.js b/packages/block-library/src/details/edit.js index 314556ba6d5919..9bebdee1a76706 100644 --- a/packages/block-library/src/details/edit.js +++ b/packages/block-library/src/details/edit.js @@ -9,9 +9,18 @@ import { InspectorControls, } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; -import { PanelBody, ToggleControl } from '@wordpress/components'; +import { + ToggleControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + const TEMPLATE = [ [ 'core/paragraph', @@ -28,6 +37,7 @@ function DetailsEdit( { attributes, setAttributes, clientId } ) { template: TEMPLATE, __experimentalCaptureToolbars: true, } ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); // Check if either the block or the inner blocks are selected. const hasSelection = useSelect( @@ -46,18 +56,37 @@ function DetailsEdit( { attributes, setAttributes, clientId } ) { return ( <> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + showContent: false, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + isShownByDefault label={ __( 'Open by default' ) } - checked={ showContent } - onChange={ () => + hasValue={ () => showContent } + onDeselect={ () => { setAttributes( { - showContent: ! showContent, - } ) - } - /> - </PanelBody> + showContent: false, + } ); + } } + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Open by default' ) } + checked={ showContent } + onChange={ () => + setAttributes( { + showContent: ! showContent, + } ) + } + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <details { ...innerBlocksProps } diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index a16d5a6c2c69c7..34be4387caad74 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -49,7 +49,6 @@ @import "./template-part/editor.scss"; @import "./text-columns/editor.scss"; @import "./video/editor.scss"; -@import "./post-template/editor.scss"; @import "./query/editor.scss"; @import "./query-pagination/editor.scss"; @import "./query-pagination-numbers/editor.scss"; diff --git a/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap index 5ce876137ade00..0c9d88a2074019 100644 --- a/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap @@ -132,7 +132,7 @@ exports[`File block renders file error state without crashing 1`] = ` <Svg height={24} style={{}} - viewBox="-2 -2 24 24" + viewBox="0 0 24 24" width={24} xmlns="http://www.w3.org/2000/svg" > diff --git a/packages/block-library/src/image/transforms.js b/packages/block-library/src/image/transforms.js index 347d2408280170..0119009b2182c4 100644 --- a/packages/block-library/src/image/transforms.js +++ b/packages/block-library/src/image/transforms.js @@ -59,6 +59,7 @@ const schema = ( { phrasingContentSchema } ) => ( { ...imageSchema, a: { attributes: [ 'href', 'rel', 'target' ], + classes: [ '*' ], children: imageSchema, }, figcaption: { diff --git a/packages/block-library/src/latest-posts/block.json b/packages/block-library/src/latest-posts/block.json index bb8c2d24962f3f..58b1c6da81ca33 100644 --- a/packages/block-library/src/latest-posts/block.json +++ b/packages/block-library/src/latest-posts/block.json @@ -111,6 +111,18 @@ "fontSize": true } }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true, + "color": true, + "width": true, + "style": true + } + }, "interactivity": { "clientNavigation": true } diff --git a/packages/block-library/src/loginout/edit.js b/packages/block-library/src/loginout/edit.js index b6c2e9cf013041..9af634c87371cf 100644 --- a/packages/block-library/src/loginout/edit.js +++ b/packages/block-library/src/loginout/edit.js @@ -1,38 +1,74 @@ /** * WordPress dependencies */ -import { PanelBody, ToggleControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; +import { + ToggleControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; export default function LoginOutEdit( { attributes, setAttributes } ) { const { displayLoginAsForm, redirectToCurrent } = attributes; + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); return ( <> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + displayLoginAsForm: false, + redirectToCurrent: true, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem label={ __( 'Display login as form' ) } - checked={ displayLoginAsForm } - onChange={ () => - setAttributes( { - displayLoginAsForm: ! displayLoginAsForm, - } ) + isShownByDefault + hasValue={ () => displayLoginAsForm } + onDeselect={ () => + setAttributes( { displayLoginAsForm: false } ) } - /> - <ToggleControl - __nextHasNoMarginBottom + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Display login as form' ) } + checked={ displayLoginAsForm } + onChange={ () => + setAttributes( { + displayLoginAsForm: ! displayLoginAsForm, + } ) + } + /> + </ToolsPanelItem> + <ToolsPanelItem label={ __( 'Redirect to current URL' ) } - checked={ redirectToCurrent } - onChange={ () => - setAttributes( { - redirectToCurrent: ! redirectToCurrent, - } ) + isShownByDefault + hasValue={ () => ! redirectToCurrent } + onDeselect={ () => + setAttributes( { redirectToCurrent: true } ) } - /> - </PanelBody> + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Redirect to current URL' ) } + checked={ redirectToCurrent } + onChange={ () => + setAttributes( { + redirectToCurrent: ! redirectToCurrent, + } ) + } + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <div { ...useBlockProps( { diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js index a946a499b26f21..820c7927303114 100644 --- a/packages/block-library/src/media-text/edit.js +++ b/packages/block-library/src/media-text/edit.js @@ -76,6 +76,7 @@ function attributesFromMedia( { mediaLink: undefined, href: undefined, focalPoint: undefined, + useFeaturedImage: false, } ); return; } @@ -128,10 +129,37 @@ function attributesFromMedia( { mediaLink: media.link || undefined, href: newHref, focalPoint: undefined, + useFeaturedImage: false, } ); }; } +function MediaTextResolutionTool( { image, value, onChange } ) { + const { imageSizes } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return { + imageSizes: getSettings().imageSizes, + }; + }, [] ); + + if ( ! imageSizes?.length ) { + return null; + } + + const imageSizeOptions = imageSizes + .filter( ( { slug } ) => getImageSourceUrlBySizeSlug( image, slug ) ) + .map( ( { name, slug } ) => ( { value: slug, label: name } ) ); + + return ( + <ResolutionTool + value={ value } + defaultValue={ DEFAULT_MEDIA_SIZE_SLUG } + options={ imageSizeOptions } + onChange={ onChange } + /> + ); +} + function MediaTextEdit( { attributes, isSelected, @@ -152,12 +180,12 @@ function MediaTextEdit( { mediaType, mediaUrl, mediaWidth, + mediaSizeSlug, rel, verticalAlignment, allowedBlocks, useFeaturedImage, } = attributes; - const mediaSizeSlug = attributes.mediaSizeSlug || DEFAULT_MEDIA_SIZE_SLUG; const [ featuredImage ] = useEntityProp( 'postType', @@ -166,11 +194,32 @@ function MediaTextEdit( { postId ); - const featuredImageMedia = useSelect( - ( select ) => - featuredImage && - select( coreStore ).getMedia( featuredImage, { context: 'view' } ), - [ featuredImage ] + const { featuredImageMedia } = useSelect( + ( select ) => { + return { + featuredImageMedia: + featuredImage && useFeaturedImage + ? select( coreStore ).getMedia( featuredImage, { + context: 'view', + } ) + : undefined, + }; + }, + [ featuredImage, useFeaturedImage ] + ); + + const { image } = useSelect( + ( select ) => { + return { + image: + mediaId && isSelected + ? select( coreStore ).getMedia( mediaId, { + context: 'view', + } ) + : null, + }; + }, + [ isSelected, mediaId ] ); const featuredImageURL = useFeaturedImage @@ -197,22 +246,6 @@ function MediaTextEdit( { } ); }; - const { imageSizes, image } = useSelect( - ( select ) => { - const { getSettings } = select( blockEditorStore ); - return { - image: - mediaId && isSelected - ? select( coreStore ).getMedia( mediaId, { - context: 'view', - } ) - : null, - imageSizes: getSettings()?.imageSizes, - }; - }, - [ isSelected, mediaId ] - ); - const refMedia = useRef(); const imperativeFocalPointPreview = ( value ) => { const { style } = refMedia.current; @@ -260,10 +293,6 @@ function MediaTextEdit( { const onVerticalAlignmentChange = ( alignment ) => { setAttributes( { verticalAlignment: alignment } ); }; - - const imageSizeOptions = imageSizes - .filter( ( { slug } ) => getImageSourceUrlBySizeSlug( image, slug ) ) - .map( ( { name, slug } ) => ( { value: slug, label: name } ) ); const updateImage = ( newMediaSizeSlug ) => { const newUrl = getImageSourceUrlBySizeSlug( image, newMediaSizeSlug ); @@ -409,9 +438,9 @@ function MediaTextEdit( { </ToolsPanelItem> ) } { mediaType === 'image' && ! useFeaturedImage && ( - <ResolutionTool + <MediaTextResolutionTool + image={ image } value={ mediaSizeSlug } - options={ imageSizeOptions } onChange={ updateImage } /> ) } diff --git a/packages/block-library/src/more/edit.js b/packages/block-library/src/more/edit.js index bcad7ec1b83662..af903640b6b8dd 100644 --- a/packages/block-library/src/more/edit.js +++ b/packages/block-library/src/more/edit.js @@ -2,10 +2,18 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { PanelBody, ToggleControl } from '@wordpress/components'; +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + ToggleControl, +} from '@wordpress/components'; import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; import { ENTER } from '@wordpress/keycodes'; import { getDefaultBlockName, createBlock } from '@wordpress/blocks'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const DEFAULT_TEXT = __( 'Read more' ); @@ -37,20 +45,39 @@ export default function MoreEdit( { width: `${ ( customText ? customText : DEFAULT_TEXT ).length + 1.2 }em`, }; + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + return ( <> <InspectorControls> - <PanelBody> - <ToggleControl - __nextHasNoMarginBottom - label={ __( - 'Hide the excerpt on the full content page' - ) } - checked={ !! noTeaser } - onChange={ toggleHideExcerpt } - help={ getHideExcerptHelp } - /> - </PanelBody> + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + noTeaser: false, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + label={ __( 'Hide excerpt' ) } + isShownByDefault + hasValue={ () => noTeaser } + onDeselect={ () => + setAttributes( { noTeaser: false } ) + } + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( + 'Hide the excerpt on the full content page' + ) } + checked={ !! noTeaser } + onChange={ toggleHideExcerpt } + help={ getHideExcerptHelp } + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <div { ...useBlockProps() }> <input diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 39073b848d3ca8..5966739aa61a61 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -9,7 +9,8 @@ import clsx from 'clsx'; import { createBlock } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; import { - PanelBody, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, TextControl, TextareaControl, ToolbarButton, @@ -161,71 +162,110 @@ function getMissingText( type ) { function Controls( { attributes, setAttributes, setIsLabelFieldFocused } ) { const { label, url, description, title, rel } = attributes; return ( - <PanelBody title={ __( 'Settings' ) }> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - value={ label ? stripHTML( label ) : '' } - onChange={ ( labelValue ) => { - setAttributes( { label: labelValue } ); - } } + <ToolsPanel label={ __( 'Settings' ) }> + <ToolsPanelItem + hasValue={ () => !! label } label={ __( 'Text' ) } - autoComplete="off" - onFocus={ () => setIsLabelFieldFocused( true ) } - onBlur={ () => setIsLabelFieldFocused( false ) } - /> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - value={ url ? safeDecodeURI( url ) : '' } - onChange={ ( urlValue ) => { - updateAttributes( - { url: urlValue }, - setAttributes, - attributes - ); - } } + onDeselect={ () => setAttributes( { label: '' } ) } + isShownByDefault + > + <TextControl + __nextHasNoMarginBottom + __next40pxDefaultSize + label={ __( 'Text' ) } + value={ label ? stripHTML( label ) : '' } + onChange={ ( labelValue ) => { + setAttributes( { label: labelValue } ); + } } + autoComplete="off" + onFocus={ () => setIsLabelFieldFocused( true ) } + onBlur={ () => setIsLabelFieldFocused( false ) } + /> + </ToolsPanelItem> + + <ToolsPanelItem + hasValue={ () => !! url } label={ __( 'Link' ) } - autoComplete="off" - /> - <TextareaControl - __nextHasNoMarginBottom - value={ description || '' } - onChange={ ( descriptionValue ) => { - setAttributes( { description: descriptionValue } ); - } } + onDeselect={ () => setAttributes( { url: '' } ) } + isShownByDefault + > + <TextControl + __nextHasNoMarginBottom + __next40pxDefaultSize + label={ __( 'Link' ) } + value={ url ? safeDecodeURI( url ) : '' } + onChange={ ( urlValue ) => { + updateAttributes( + { url: urlValue }, + setAttributes, + attributes + ); + } } + autoComplete="off" + /> + </ToolsPanelItem> + + <ToolsPanelItem + hasValue={ () => !! description } label={ __( 'Description' ) } - help={ __( - 'The description will be displayed in the menu if the current theme supports it.' - ) } - /> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - value={ title || '' } - onChange={ ( titleValue ) => { - setAttributes( { title: titleValue } ); - } } + onDeselect={ () => setAttributes( { description: '' } ) } + isShownByDefault + > + <TextareaControl + __nextHasNoMarginBottom + label={ __( 'Description' ) } + value={ description || '' } + onChange={ ( descriptionValue ) => { + setAttributes( { description: descriptionValue } ); + } } + help={ __( + 'The description will be displayed in the menu if the current theme supports it.' + ) } + /> + </ToolsPanelItem> + + <ToolsPanelItem + hasValue={ () => !! title } label={ __( 'Title attribute' ) } - autoComplete="off" - help={ __( - 'Additional information to help clarify the purpose of the link.' - ) } - /> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - value={ rel || '' } - onChange={ ( relValue ) => { - setAttributes( { rel: relValue } ); - } } + onDeselect={ () => setAttributes( { title: '' } ) } + isShownByDefault + > + <TextControl + __nextHasNoMarginBottom + __next40pxDefaultSize + label={ __( 'Title attribute' ) } + value={ title || '' } + onChange={ ( titleValue ) => { + setAttributes( { title: titleValue } ); + } } + autoComplete="off" + help={ __( + 'Additional information to help clarify the purpose of the link.' + ) } + /> + </ToolsPanelItem> + + <ToolsPanelItem + hasValue={ () => !! rel } label={ __( 'Rel attribute' ) } - autoComplete="off" - help={ __( - 'The relationship of the linked URL as space-separated link types.' - ) } - /> - </PanelBody> + onDeselect={ () => setAttributes( { rel: '' } ) } + isShownByDefault + > + <TextControl + __nextHasNoMarginBottom + __next40pxDefaultSize + label={ __( 'Rel attribute' ) } + value={ rel || '' } + onChange={ ( relValue ) => { + setAttributes( { rel: relValue } ); + } } + autoComplete="off" + help={ __( + 'The relationship of the linked URL as space-separated link types.' + ) } + /> + </ToolsPanelItem> + </ToolsPanel> ); } diff --git a/packages/block-library/src/navigation-link/index.php b/packages/block-library/src/navigation-link/index.php index 5653e04fca88a3..81df2099dfc188 100644 --- a/packages/block-library/src/navigation-link/index.php +++ b/packages/block-library/src/navigation-link/index.php @@ -177,7 +177,22 @@ function render_block_core_navigation_link( $attributes, $content, $block ) { // Don't render the block's subtree if it is a draft or if the ID does not exist. if ( $is_post_type && $navigation_link_has_id ) { $post = get_post( $attributes['id'] ); - if ( ! $post || 'publish' !== $post->post_status ) { + /** + * Filter allowed post_status for navigation link block to render. + * + * @since 6.8.0 + * + * @param array $post_status + * @param array $attributes + * @param WP_Block $block + */ + $allowed_post_status = (array) apply_filters( + 'render_block_core_navigation_link_allowed_post_status', + array( 'publish' ), + $attributes, + $block + ); + if ( ! $post || ! in_array( $post->post_status, $allowed_post_status, true ) ) { return ''; } } diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index c89eadf1cb589e..315169f2736adf 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -8,11 +8,12 @@ import clsx from 'clsx'; */ import { useSelect, useDispatch } from '@wordpress/data'; import { - PanelBody, TextControl, TextareaControl, ToolbarButton, ToolbarGroup, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { displayShortcut, isKeyboardEvent } from '@wordpress/keycodes'; import { __ } from '@wordpress/i18n'; @@ -43,6 +44,7 @@ import { getColors, getNavigationChildBlockProps, } from '../navigation/edit/utils'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const ALLOWED_BLOCKS = [ 'core/navigation-link', @@ -152,6 +154,7 @@ export default function NavigationSubmenuEdit( { const isDraggingWithin = useIsDraggingWithin( listItemRef ); const itemLabelPlaceholder = __( 'Add text…' ); const ref = useRef(); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); const { parentCount, @@ -382,67 +385,120 @@ export default function NavigationSubmenuEdit( { </BlockControls> { /* Warning, this duplicated in packages/block-library/src/navigation-link/edit.js */ } <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - value={ label || '' } - onChange={ ( labelValue ) => { - setAttributes( { label: labelValue } ); - } } + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + label: '', + url: '', + description: '', + title: '', + rel: '', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem label={ __( 'Text' ) } - autoComplete="off" - /> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - value={ url || '' } - onChange={ ( urlValue ) => { - setAttributes( { url: urlValue } ); - } } + isShownByDefault + hasValue={ () => !! label } + onDeselect={ () => setAttributes( { label: '' } ) } + > + <TextControl + __nextHasNoMarginBottom + __next40pxDefaultSize + value={ label || '' } + onChange={ ( labelValue ) => { + setAttributes( { label: labelValue } ); + } } + label={ __( 'Text' ) } + autoComplete="off" + /> + </ToolsPanelItem> + + <ToolsPanelItem label={ __( 'Link' ) } - autoComplete="off" - /> - <TextareaControl - __nextHasNoMarginBottom - value={ description || '' } - onChange={ ( descriptionValue ) => { - setAttributes( { - description: descriptionValue, - } ); - } } + isShownByDefault + hasValue={ () => !! url } + onDeselect={ () => setAttributes( { url: '' } ) } + > + <TextControl + __nextHasNoMarginBottom + __next40pxDefaultSize + value={ url || '' } + onChange={ ( urlValue ) => { + setAttributes( { url: urlValue } ); + } } + label={ __( 'Link' ) } + autoComplete="off" + /> + </ToolsPanelItem> + + <ToolsPanelItem label={ __( 'Description' ) } - help={ __( - 'The description will be displayed in the menu if the current theme supports it.' - ) } - /> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - value={ title || '' } - onChange={ ( titleValue ) => { - setAttributes( { title: titleValue } ); - } } + isShownByDefault + hasValue={ () => !! description } + onDeselect={ () => + setAttributes( { description: '' } ) + } + > + <TextareaControl + __nextHasNoMarginBottom + value={ description || '' } + onChange={ ( descriptionValue ) => { + setAttributes( { + description: descriptionValue, + } ); + } } + label={ __( 'Description' ) } + help={ __( + 'The description will be displayed in the menu if the current theme supports it.' + ) } + /> + </ToolsPanelItem> + + <ToolsPanelItem label={ __( 'Title attribute' ) } - autoComplete="off" - help={ __( - 'Additional information to help clarify the purpose of the link.' - ) } - /> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - value={ rel || '' } - onChange={ ( relValue ) => { - setAttributes( { rel: relValue } ); - } } + isShownByDefault + hasValue={ () => !! title } + onDeselect={ () => setAttributes( { title: '' } ) } + > + <TextControl + __nextHasNoMarginBottom + __next40pxDefaultSize + value={ title || '' } + onChange={ ( titleValue ) => { + setAttributes( { title: titleValue } ); + } } + label={ __( 'Title attribute' ) } + autoComplete="off" + help={ __( + 'Additional information to help clarify the purpose of the link.' + ) } + /> + </ToolsPanelItem> + + <ToolsPanelItem label={ __( 'Rel attribute' ) } - autoComplete="off" - help={ __( - 'The relationship of the linked URL as space-separated link types.' - ) } - /> - </PanelBody> + isShownByDefault + hasValue={ () => !! rel } + onDeselect={ () => setAttributes( { rel: '' } ) } + > + <TextControl + __nextHasNoMarginBottom + __next40pxDefaultSize + value={ rel || '' } + onChange={ ( relValue ) => { + setAttributes( { rel: relValue } ); + } } + label={ __( 'Rel attribute' ) } + autoComplete="off" + help={ __( + 'The relationship of the linked URL as space-separated link types.' + ) } + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <div { ...blockProps }> { /* eslint-disable jsx-a11y/anchor-is-valid */ } diff --git a/packages/block-library/src/navigation/edit/navigation-menu-selector.js b/packages/block-library/src/navigation/edit/navigation-menu-selector.js index dceabf063b26e8..0efb597ff85324 100644 --- a/packages/block-library/src/navigation/edit/navigation-menu-selector.js +++ b/packages/block-library/src/navigation/edit/navigation-menu-selector.js @@ -61,7 +61,8 @@ function NavigationMenuSelector( { hasResolvedNavigationMenus, canUserCreateNavigationMenus, canSwitchNavigationMenu, - } = useNavigationMenu(); + isNavigationMenuMissing, + } = useNavigationMenu( currentMenuId ); const [ currentTitle ] = useEntityProp( 'postType', @@ -106,12 +107,18 @@ function NavigationMenuSelector( { const noBlockMenus = ! hasNavigationMenus && hasResolvedNavigationMenus; const menuUnavailable = hasResolvedNavigationMenus && currentMenuId === null; + const navMenuHasBeenDeleted = currentMenuId && isNavigationMenuMissing; let selectorLabel = ''; if ( isResolvingNavigationMenus ) { selectorLabel = __( 'Loading…' ); - } else if ( noMenuSelected || noBlockMenus || menuUnavailable ) { + } else if ( + noMenuSelected || + noBlockMenus || + menuUnavailable || + navMenuHasBeenDeleted + ) { // Note: classic Menus may be available. selectorLabel = __( 'Choose or create a Navigation Menu' ); } else { diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 9a56e399fcfecb..43ca8331534275 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -539,8 +539,8 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $inner_blocks_html, $toggle_aria_label_open, $toggle_aria_label_close, - esc_attr( implode( ' ', $responsive_container_classes ) ), - esc_attr( implode( ' ', $open_button_classes ) ), + esc_attr( trim( implode( ' ', $responsive_container_classes ) ) ), + esc_attr( trim( implode( ' ', $open_button_classes ) ) ), ( ! empty( $overlay_inline_styles ) ) ? "style=\"$overlay_inline_styles\"" : '', $toggle_button_content, $toggle_close_button_content, diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index 31e400b8676717..8f1409f864f9b9 100644 --- a/packages/block-library/src/page-list/edit.js +++ b/packages/block-library/src/page-list/edit.js @@ -17,12 +17,13 @@ import { Warning, } from '@wordpress/block-editor'; import { - PanelBody, ToolbarButton, Spinner, Notice, ComboboxControl, Button, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useMemo, useState, useEffect, useCallback } from '@wordpress/element'; @@ -37,6 +38,7 @@ import { convertDescription, ConvertToLinksModal, } from './convert-to-links-modal'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; // We only show the edit option when page count is <= MAX_PAGE_COUNT // Performance of Navigation Links is not good past this value. @@ -123,6 +125,7 @@ export default function PageListEdit( { const [ isOpen, setOpen ] = useState( false ); const openModal = useCallback( () => setOpen( true ), [] ); const closeModal = () => setOpen( false ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); const { records: pages, hasResolved: hasResolvedPages } = useEntityRecords( 'postType', @@ -320,38 +323,56 @@ export default function PageListEdit( { return ( <> <InspectorControls> - { pagesTree.length > 0 && ( - <PanelBody> - <ComboboxControl - __nextHasNoMarginBottom - __next40pxDefaultSize - className="editor-page-attributes__parent" - label={ __( 'Parent' ) } - value={ parentPageID } - options={ pagesTree } - onChange={ ( value ) => - setAttributes( { parentPageID: value ?? 0 } ) + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { parentPageID: 0 } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + { pagesTree.length > 0 && ( + <ToolsPanelItem + label={ __( 'Parent Page' ) } + hasValue={ () => parentPageID !== 0 } + onDeselect={ () => + setAttributes( { parentPageID: 0 } ) } - help={ __( - 'Choose a page to show only its subpages.' - ) } - /> - </PanelBody> - ) } - { allowConvertToLinks && ( - <PanelBody title={ __( 'Edit this menu' ) }> - <p>{ convertDescription }</p> - <Button - __next40pxDefaultSize - variant="primary" - accessibleWhenDisabled - disabled={ ! hasResolvedPages } - onClick={ convertToNavigationLinks } + isShownByDefault > - { __( 'Edit' ) } - </Button> - </PanelBody> - ) } + <ComboboxControl + __nextHasNoMarginBottom + __next40pxDefaultSize + className="editor-page-attributes__parent" + label={ __( 'Parent' ) } + value={ parentPageID } + options={ pagesTree } + onChange={ ( value ) => + setAttributes( { + parentPageID: value ?? 0, + } ) + } + help={ __( + 'Choose a page to show only its subpages.' + ) } + /> + </ToolsPanelItem> + ) } + + { allowConvertToLinks && ( + <div style={ { gridColumn: '1 / -1' } }> + <p>{ convertDescription }</p> + <Button + __next40pxDefaultSize + variant="primary" + accessibleWhenDisabled + disabled={ ! hasResolvedPages } + onClick={ convertToNavigationLinks } + > + { __( 'Edit' ) } + </Button> + </div> + ) } + </ToolsPanel> </InspectorControls> { allowConvertToLinks && ( <> diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 02ca1feceae555..f1c2e15537b99b 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -20,14 +20,16 @@ import { useBlockProps, useSettings, useBlockEditingMode, + store as blockEditorStore, } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; import { getBlockSupport } from '@wordpress/blocks'; import { formatLtr } from '@wordpress/icons'; - /** * Internal dependencies */ import { useOnEnter } from './use-enter'; +import { unlock } from '../lock-unlock'; function ParagraphRTLControl( { direction, setDirection } ) { return ( @@ -109,7 +111,11 @@ function ParagraphBlock( { isSelected: isSingleSelected, name, } ) { - const { align, content, direction, dropCap, placeholder } = attributes; + const isZoomOut = useSelect( ( select ) => + unlock( select( blockEditorStore ) ).isZoomOut() + ); + + const { align, content, direction, dropCap } = attributes; const blockProps = useBlockProps( { ref: useOnEnter( { clientId, content } ), className: clsx( { @@ -119,6 +125,12 @@ function ParagraphBlock( { style: { direction }, } ); const blockEditingMode = useBlockEditingMode(); + let { placeholder } = attributes; + if ( isZoomOut ) { + placeholder = ''; + } else if ( ! placeholder ) { + placeholder = __( 'Type / to choose a block' ); + } return ( <> @@ -170,8 +182,10 @@ function ParagraphBlock( { : __( 'Block: Paragraph' ) } data-empty={ RichText.isEmpty( content ) } - placeholder={ placeholder || __( 'Type / to choose a block' ) } - data-custom-placeholder={ placeholder ? true : undefined } + placeholder={ placeholder } + data-custom-placeholder={ + placeholder && ! isZoomOut ? true : undefined + } __unstableEmbedURLOnPaste __unstableAllowPrefixTransformations /> diff --git a/packages/block-library/src/post-author-name/block.json b/packages/block-library/src/post-author-name/block.json index 68d2c49bd91056..23211f0bf5bf46 100644 --- a/packages/block-library/src/post-author-name/block.json +++ b/packages/block-library/src/post-author-name/block.json @@ -12,11 +12,13 @@ }, "isLink": { "type": "boolean", - "default": false + "default": false, + "role": "content" }, "linkTarget": { "type": "string", - "default": "_self" + "default": "_self", + "role": "content" } }, "usesContext": [ "postType", "postId" ], diff --git a/packages/block-library/src/post-author-name/edit.js b/packages/block-library/src/post-author-name/edit.js index b4afb9a9799498..2b4bb0709356b0 100644 --- a/packages/block-library/src/post-author-name/edit.js +++ b/packages/block-library/src/post-author-name/edit.js @@ -13,7 +13,7 @@ import { useBlockProps, } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; import { PanelBody, ToggleControl } from '@wordpress/components'; @@ -22,9 +22,10 @@ function PostAuthorNameEdit( { attributes: { textAlign, isLink, linkTarget }, setAttributes, } ) { - const { authorName } = useSelect( + const { authorName, supportsAuthor } = useSelect( ( select ) => { - const { getEditedEntityRecord, getUser } = select( coreStore ); + const { getEditedEntityRecord, getUser, getPostType } = + select( coreStore ); const _authorId = getEditedEntityRecord( 'postType', postType, @@ -33,6 +34,8 @@ function PostAuthorNameEdit( { return { authorName: _authorId ? getUser( _authorId ) : null, + supportsAuthor: + getPostType( postType )?.supports?.author ?? false, }; }, [ postType, postId ] @@ -90,7 +93,17 @@ function PostAuthorNameEdit( { ) } </PanelBody> </InspectorControls> - <div { ...blockProps }> { displayAuthor } </div> + <div { ...blockProps }> + { supportsAuthor + ? displayAuthor + : sprintf( + // translators: %s: Name of the post type e.g: "post". + __( + 'This post type (%s) does not support the author.' + ), + postType + ) } + </div> </> ); } diff --git a/packages/block-library/src/post-author-name/index.php b/packages/block-library/src/post-author-name/index.php index effc83962a3547..243d78ca70129e 100644 --- a/packages/block-library/src/post-author-name/index.php +++ b/packages/block-library/src/post-author-name/index.php @@ -26,6 +26,10 @@ function render_block_core_post_author_name( $attributes, $content, $block ) { return ''; } + if ( ! post_type_supports( $block->context['postType'], 'author' ) ) { + return ''; + } + $author_name = get_the_author_meta( 'display_name', $author_id ); if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) { $author_name = sprintf( '<a href="%1$s" target="%2$s" class="wp-block-post-author-name__link">%3$s</a>', get_author_posts_url( $author_id ), esc_attr( $attributes['linkTarget'] ), $author_name ); diff --git a/packages/block-library/src/post-author/block.json b/packages/block-library/src/post-author/block.json index d66498c8ee3df9..c7f2f01550a613 100644 --- a/packages/block-library/src/post-author/block.json +++ b/packages/block-library/src/post-author/block.json @@ -26,11 +26,13 @@ }, "isLink": { "type": "boolean", - "default": false + "default": false, + "role": "content" }, "linkTarget": { "type": "string", - "default": "_self" + "default": "_self", + "role": "content" } }, "usesContext": [ "postType", "postId", "queryId" ], diff --git a/packages/block-library/src/post-author/edit.js b/packages/block-library/src/post-author/edit.js index 6186b0d052e8aa..dd2b3aa617548d 100644 --- a/packages/block-library/src/post-author/edit.js +++ b/packages/block-library/src/post-author/edit.js @@ -21,7 +21,7 @@ import { __experimentalVStack as VStack, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; const minimumUsersForCombobox = 25; @@ -38,9 +38,9 @@ function PostAuthorEdit( { setAttributes, } ) { const isDescendentOfQueryLoop = Number.isFinite( queryId ); - const { authorId, authorDetails, authors } = useSelect( + const { authorId, authorDetails, authors, supportsAuthor } = useSelect( ( select ) => { - const { getEditedEntityRecord, getUser, getUsers } = + const { getEditedEntityRecord, getUser, getUsers, getPostType } = select( coreStore ); const _authorId = getEditedEntityRecord( 'postType', @@ -52,6 +52,8 @@ function PostAuthorEdit( { authorId: _authorId, authorDetails: _authorId ? getUser( _authorId ) : null, authors: getUsers( AUTHORS_QUERY ), + supportsAuthor: + getPostType( postType )?.supports?.author ?? false, }; }, [ postType, postId ] @@ -97,6 +99,18 @@ function PostAuthorEdit( { const showAuthorControl = !! postId && ! isDescendentOfQueryLoop && authorOptions.length > 0; + if ( ! supportsAuthor ) { + return ( + <div { ...blockProps }> + { sprintf( + // translators: %s: Name of the post type e.g: "post". + __( 'This post type (%s) does not support the author.' ), + postType + ) } + </div> + ); + } + return ( <> <InspectorControls> diff --git a/packages/block-library/src/post-author/index.php b/packages/block-library/src/post-author/index.php index faf894d997d732..2d01de508b94af 100644 --- a/packages/block-library/src/post-author/index.php +++ b/packages/block-library/src/post-author/index.php @@ -26,6 +26,10 @@ function render_block_core_post_author( $attributes, $content, $block ) { return ''; } + if ( ! post_type_supports( $block->context['postType'], 'author' ) ) { + return ''; + } + $avatar = ! empty( $attributes['avatarSize'] ) ? get_avatar( $author_id, $attributes['avatarSize'] diff --git a/packages/block-library/src/post-comments-form/block.json b/packages/block-library/src/post-comments-form/block.json index af893ccb67a082..4b6b333b75cfab 100644 --- a/packages/block-library/src/post-comments-form/block.json +++ b/packages/block-library/src/post-comments-form/block.json @@ -56,5 +56,10 @@ "wp-block-post-comments-form", "wp-block-buttons", "wp-block-button" - ] + ], + "example": { + "attributes": { + "textAlign": "center" + } + } } diff --git a/packages/block-library/src/post-comments-link/block.json b/packages/block-library/src/post-comments-link/block.json index 67831b1d15c5d5..8e23bc7a695070 100644 --- a/packages/block-library/src/post-comments-link/block.json +++ b/packages/block-library/src/post-comments-link/block.json @@ -42,6 +42,13 @@ }, "interactivity": { "clientNavigation": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true } - } + }, + "style": "wp-block-post-comments-link" } diff --git a/packages/block-library/src/post-comments-link/style.scss b/packages/block-library/src/post-comments-link/style.scss new file mode 100644 index 00000000000000..110179d3ee1df9 --- /dev/null +++ b/packages/block-library/src/post-comments-link/style.scss @@ -0,0 +1,4 @@ +.wp-block-post-comments-link { + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; +} diff --git a/packages/block-library/src/post-content/index.php b/packages/block-library/src/post-content/index.php index 25be880cc47887..e0a06b7217eebe 100644 --- a/packages/block-library/src/post-content/index.php +++ b/packages/block-library/src/post-content/index.php @@ -46,10 +46,33 @@ function render_block_core_post_content( $attributes, $content, $block ) { $content .= wp_link_pages( array( 'echo' => 0 ) ); } + $ignored_hooked_blocks = get_post_meta( $post_id, '_wp_ignored_hooked_blocks', true ); + if ( ! empty( $ignored_hooked_blocks ) ) { + $ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true ); + $attributes['metadata'] = array( + 'ignoredHookedBlocks' => $ignored_hooked_blocks, + ); + } + + // Wrap in Post Content block so the Block Hooks algorithm can insert blocks + // that are hooked as first or last child of `core/post-content`. + $content = get_comment_delimited_block_content( + 'core/post-content', + $attributes, + $content + ); + + // We need to remove the `core/post-content` block wrapper after the Block Hooks algorithm, + // but before `do_blocks` runs, as it would otherwise attempt to render the same block again -- + // thus recursing infinitely. + add_filter( 'the_content', 'remove_serialized_parent_block', 8 ); + /** This filter is documented in wp-includes/post-template.php */ $content = apply_filters( 'the_content', str_replace( ']]>', ']]&gt;', $content ) ); unset( $seen_ids[ $post_id ] ); + remove_filter( 'the_content', 'remove_serialized_parent_block', 8 ); + if ( empty( $content ) ) { return ''; } diff --git a/packages/block-library/src/post-date/block.json b/packages/block-library/src/post-date/block.json index 470bddae53bdfc..dadc0d2f489fee 100644 --- a/packages/block-library/src/post-date/block.json +++ b/packages/block-library/src/post-date/block.json @@ -15,7 +15,8 @@ }, "isLink": { "type": "boolean", - "default": false + "default": false, + "role": "content" }, "displayType": { "type": "string", diff --git a/packages/block-library/src/post-date/edit.js b/packages/block-library/src/post-date/edit.js index 5057466c6af453..36de2f7e5d7255 100644 --- a/packages/block-library/src/post-date/edit.js +++ b/packages/block-library/src/post-date/edit.js @@ -26,13 +26,19 @@ import { ToolbarGroup, ToolbarButton, ToggleControl, - PanelBody, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { __, _x, sprintf } from '@wordpress/i18n'; import { edit } from '@wordpress/icons'; import { DOWN } from '@wordpress/keycodes'; import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + export default function PostDateEdit( { attributes: { textAlign, format, isLink, displayType }, context: { postId, postType: postTypeSlug, queryId }, @@ -44,6 +50,7 @@ export default function PostDateEdit( { [ `wp-block-post-date__modified-date` ]: displayType === 'modified', } ), } ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); // Use internal state instead of a ref to make sure that the component // re-renders when the popover's anchor updates. @@ -160,16 +167,37 @@ export default function PostDateEdit( { </BlockControls> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <DateFormatPicker - format={ format } - defaultFormat={ siteFormat } - onChange={ ( nextFormat ) => - setAttributes( { format: nextFormat } ) + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + format: undefined, + isLink: false, + displayType: 'date', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + hasValue={ () => + format !== undefined && format !== siteFormat + } + label={ __( 'Date Format' ) } + onDeselect={ () => + setAttributes( { format: undefined } ) } - /> - <ToggleControl - __nextHasNoMarginBottom + isShownByDefault + > + <DateFormatPicker + format={ format } + defaultFormat={ siteFormat } + onChange={ ( nextFormat ) => + setAttributes( { format: nextFormat } ) + } + /> + </ToolsPanelItem> + <ToolsPanelItem + hasValue={ () => isLink !== false } label={ postType?.labels.singular_name ? sprintf( @@ -179,23 +207,49 @@ export default function PostDateEdit( { ) : __( 'Link to post' ) } - onChange={ () => setAttributes( { isLink: ! isLink } ) } - checked={ isLink } - /> - <ToggleControl - __nextHasNoMarginBottom + onDeselect={ () => setAttributes( { isLink: false } ) } + isShownByDefault + > + <ToggleControl + __nextHasNoMarginBottom + label={ + postType?.labels.singular_name + ? sprintf( + // translators: %s: Name of the post type e.g: "post". + __( 'Link to %s' ), + postType.labels.singular_name.toLowerCase() + ) + : __( 'Link to post' ) + } + onChange={ () => + setAttributes( { isLink: ! isLink } ) + } + checked={ isLink } + /> + </ToolsPanelItem> + <ToolsPanelItem + hasValue={ () => displayType !== 'date' } label={ __( 'Display last modified date' ) } - onChange={ ( value ) => - setAttributes( { - displayType: value ? 'modified' : 'date', - } ) + onDeselect={ () => + setAttributes( { displayType: 'date' } ) } - checked={ displayType === 'modified' } - help={ __( - 'Only shows if the post has been modified' - ) } - /> - </PanelBody> + isShownByDefault + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Display last modified date' ) } + onChange={ ( value ) => + setAttributes( { + displayType: value ? 'modified' : 'date', + } ) + } + checked={ displayType === 'modified' } + help={ __( + 'Only shows if the post has been modified' + ) } + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <div { ...blockProps }>{ postDate }</div> diff --git a/packages/block-library/src/post-excerpt/edit.js b/packages/block-library/src/post-excerpt/edit.js index 05aaf543b59196..bc94c2599e60ec 100644 --- a/packages/block-library/src/post-excerpt/edit.js +++ b/packages/block-library/src/post-excerpt/edit.js @@ -16,14 +16,22 @@ import { Warning, useBlockProps, } from '@wordpress/block-editor'; -import { PanelBody, ToggleControl, RangeControl } from '@wordpress/components'; +import { + ToggleControl, + RangeControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { useCanEditEntity } from '../utils/hooks'; +import { + useCanEditEntity, + useToolsPanelDropdownMenuProps, +} from '../utils/hooks'; const ELLIPSIS = '…'; @@ -41,6 +49,8 @@ export default function PostExcerptEditor( { { rendered: renderedExcerpt, protected: isProtected } = {}, ] = useEntityProp( 'postType', postType, 'excerpt', postId ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + /** * Check if the post type supports excerpts. * Add an exception and return early for the "page" post type, @@ -219,29 +229,56 @@ export default function PostExcerptEditor( { /> </BlockControls> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + showMoreOnNewLine: true, + excerptLength: 55, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + hasValue={ () => showMoreOnNewLine !== true } label={ __( 'Show link on new line' ) } - checked={ showMoreOnNewLine } - onChange={ ( newShowMoreOnNewLine ) => - setAttributes( { - showMoreOnNewLine: newShowMoreOnNewLine, - } ) + onDeselect={ () => + setAttributes( { showMoreOnNewLine: true } ) } - /> - <RangeControl - __next40pxDefaultSize - __nextHasNoMarginBottom + isShownByDefault + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Show link on new line' ) } + checked={ showMoreOnNewLine } + onChange={ ( newShowMoreOnNewLine ) => + setAttributes( { + showMoreOnNewLine: newShowMoreOnNewLine, + } ) + } + /> + </ToolsPanelItem> + <ToolsPanelItem + hasValue={ () => excerptLength !== 55 } label={ __( 'Max number of words' ) } - value={ excerptLength } - onChange={ ( value ) => { - setAttributes( { excerptLength: value } ); - } } - min="10" - max="100" - /> - </PanelBody> + onDeselect={ () => + setAttributes( { excerptLength: 55 } ) + } + isShownByDefault + > + <RangeControl + __next40pxDefaultSize + __nextHasNoMarginBottom + label={ __( 'Max number of words' ) } + value={ excerptLength } + onChange={ ( value ) => { + setAttributes( { excerptLength: value } ); + } } + min="10" + max="100" + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <div { ...blockProps }> { excerptContent } diff --git a/packages/block-library/src/post-featured-image/block.json b/packages/block-library/src/post-featured-image/block.json index 8b431ffc625790..3cd144caa0cf42 100644 --- a/packages/block-library/src/post-featured-image/block.json +++ b/packages/block-library/src/post-featured-image/block.json @@ -9,7 +9,8 @@ "attributes": { "isLink": { "type": "boolean", - "default": false + "default": false, + "role": "content" }, "aspectRatio": { "type": "string" @@ -30,11 +31,13 @@ "rel": { "type": "string", "attribute": "rel", - "default": "" + "default": "", + "role": "content" }, "linkTarget": { "type": "string", - "default": "_self" + "default": "_self", + "role": "content" }, "overlayColor": { "type": "string" diff --git a/packages/block-library/src/post-featured-image/dimension-controls.js b/packages/block-library/src/post-featured-image/dimension-controls.js index 5a3e40a126bf8d..9a71a96b2db846 100644 --- a/packages/block-library/src/post-featured-image/dimension-controls.js +++ b/packages/block-library/src/post-featured-image/dimension-controls.js @@ -12,10 +12,18 @@ import { } from '@wordpress/components'; import { useSettings, + privateApis as blockEditorPrivateApis, store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; + +const { ResolutionTool } = unlock( blockEditorPrivateApis ); + const SCALE_OPTIONS = ( <> <ToggleGroupControlOption @@ -223,30 +231,19 @@ const DimensionControls = ( { </ToolsPanelItem> ) } { !! imageSizeOptions.length && ( - <ToolsPanelItem - hasValue={ () => !! sizeSlug } - label={ __( 'Resolution' ) } - onDeselect={ () => - setAttributes( { sizeSlug: undefined } ) + <ResolutionTool + panelId={ clientId } + value={ sizeSlug } + defaultValue={ DEFAULT_SIZE } + options={ imageSizeOptions } + onChange={ ( nextSizeSlug ) => + setAttributes( { sizeSlug: nextSizeSlug } ) } + isShownByDefault={ false } resetAllFilter={ () => ( { - sizeSlug: undefined, + sizeSlug: DEFAULT_SIZE, } ) } - isShownByDefault={ false } - panelId={ clientId } - > - <SelectControl - __next40pxDefaultSize - __nextHasNoMarginBottom - label={ __( 'Resolution' ) } - value={ sizeSlug || DEFAULT_SIZE } - options={ imageSizeOptions } - onChange={ ( nextSizeSlug ) => - setAttributes( { sizeSlug: nextSizeSlug } ) - } - help={ __( 'Select the size of the source image.' ) } - /> - </ToolsPanelItem> + /> ) } </> ); diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index 95441a5a55cfd0..05888c41fecf23 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -11,11 +11,12 @@ import { useEntityProp, store as coreStore } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; import { ToggleControl, - PanelBody, Placeholder, Button, Spinner, TextControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { InspectorControls, @@ -38,6 +39,7 @@ import { store as noticesStore } from '@wordpress/notices'; import DimensionControls from './dimension-controls'; import OverlayControls from './overlay-controls'; import Overlay from './overlay'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; @@ -183,6 +185,8 @@ export default function PostFeaturedImageEdit( { setTemporaryURL(); }; + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + const controls = blockEditingMode === 'default' && ( <> <InspectorControls group="color"> @@ -201,9 +205,18 @@ export default function PostFeaturedImageEdit( { /> </InspectorControls> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + isLink: false, + linkTarget: '_self', + rel: '', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem label={ postType?.labels.singular_name ? sprintf( @@ -213,11 +226,42 @@ export default function PostFeaturedImageEdit( { ) : __( 'Link to post' ) } - onChange={ () => setAttributes( { isLink: ! isLink } ) } - checked={ isLink } - /> + isShownByDefault + hasValue={ () => !! isLink } + onDeselect={ () => + setAttributes( { + isLink: false, + } ) + } + > + <ToggleControl + __nextHasNoMarginBottom + label={ + postType?.labels.singular_name + ? sprintf( + // translators: %s: Name of the post type e.g: "post". + __( 'Link to %s' ), + postType.labels.singular_name + ) + : __( 'Link to post' ) + } + onChange={ () => + setAttributes( { isLink: ! isLink } ) + } + checked={ isLink } + /> + </ToolsPanelItem> { isLink && ( - <> + <ToolsPanelItem + label={ __( 'Open in new tab' ) } + isShownByDefault + hasValue={ () => '_self' !== linkTarget } + onDeselect={ () => + setAttributes( { + linkTarget: '_self', + } ) + } + > <ToggleControl __nextHasNoMarginBottom label={ __( 'Open in new tab' ) } @@ -228,6 +272,19 @@ export default function PostFeaturedImageEdit( { } checked={ linkTarget === '_blank' } /> + </ToolsPanelItem> + ) } + { isLink && ( + <ToolsPanelItem + label={ __( 'Link rel' ) } + isShownByDefault + hasValue={ () => !! rel } + onDeselect={ () => + setAttributes( { + rel: '', + } ) + } + > <TextControl __next40pxDefaultSize __nextHasNoMarginBottom @@ -237,9 +294,9 @@ export default function PostFeaturedImageEdit( { setAttributes( { rel: newRel } ) } /> - </> + </ToolsPanelItem> ) } - </PanelBody> + </ToolsPanel> </InspectorControls> </> ); diff --git a/packages/block-library/src/post-navigation-link/block.json b/packages/block-library/src/post-navigation-link/block.json index 5f1b295119822a..ce733759846fee 100644 --- a/packages/block-library/src/post-navigation-link/block.json +++ b/packages/block-library/src/post-navigation-link/block.json @@ -34,12 +34,6 @@ "default": "" } }, - "example": { - "attributes": { - "label": "Next post", - "arrow": "arrow" - } - }, "usesContext": [ "postType" ], "supports": { "reusable": false, diff --git a/packages/block-library/src/post-navigation-link/index.js b/packages/block-library/src/post-navigation-link/index.js index e85e594990adba..4bcb1999067053 100644 --- a/packages/block-library/src/post-navigation-link/index.js +++ b/packages/block-library/src/post-navigation-link/index.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ @@ -12,6 +17,12 @@ export { metadata, name }; export const settings = { edit, variations, + example: { + attributes: { + label: __( 'Next post' ), + arrow: 'arrow', + }, + }, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/post-navigation-link/variations.js b/packages/block-library/src/post-navigation-link/variations.js index 4f52b21338af1e..e49be1542685e7 100644 --- a/packages/block-library/src/post-navigation-link/variations.js +++ b/packages/block-library/src/post-navigation-link/variations.js @@ -17,7 +17,7 @@ const variations = [ scope: [ 'inserter', 'transform' ], example: { attributes: { - label: 'Next post', + label: __( 'Next post' ), arrow: 'arrow', }, }, @@ -33,7 +33,7 @@ const variations = [ scope: [ 'inserter', 'transform' ], example: { attributes: { - label: 'Previous post', + label: __( 'Previous post' ), arrow: 'arrow', }, }, diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json index 6e1f58155590f3..d379a46d3142f8 100644 --- a/packages/block-library/src/post-template/block.json +++ b/packages/block-library/src/post-template/block.json @@ -43,15 +43,25 @@ } }, "spacing": { + "margin": true, + "padding": true, "blockGap": { "__experimentalDefault": "1.25em" }, "__experimentalDefaultControls": { - "blockGap": true + "blockGap": true, + "padding": false, + "margin": false } }, "interactivity": { "clientNavigation": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true } }, "style": "wp-block-post-template", diff --git a/packages/block-library/src/post-template/editor.scss b/packages/block-library/src/post-template/editor.scss deleted file mode 100644 index 7b426b0f3d37a5..00000000000000 --- a/packages/block-library/src/post-template/editor.scss +++ /dev/null @@ -1,7 +0,0 @@ -.editor-styles-wrapper { - ul.wp-block-post-template { - padding-left: 0; - margin-left: 0; - list-style: none; - } -} diff --git a/packages/block-library/src/post-template/style.scss b/packages/block-library/src/post-template/style.scss index 806aadc77470eb..e6896f2db024a8 100644 --- a/packages/block-library/src/post-template/style.scss +++ b/packages/block-library/src/post-template/style.scss @@ -4,6 +4,8 @@ max-width: 100%; list-style: none; padding: 0; + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; // These rules no longer apply but should be kept for backwards compatibility. &.is-flex-container { diff --git a/packages/block-library/src/post-title/block.json b/packages/block-library/src/post-title/block.json index ecb5053d6cd39e..5587d71b148d0c 100644 --- a/packages/block-library/src/post-title/block.json +++ b/packages/block-library/src/post-title/block.json @@ -20,16 +20,19 @@ }, "isLink": { "type": "boolean", - "default": false + "default": false, + "role": "content" }, "rel": { "type": "string", "attribute": "rel", - "default": "" + "default": "", + "role": "content" }, "linkTarget": { "type": "string", - "default": "_self" + "default": "_self", + "role": "content" } }, "example": { diff --git a/packages/block-library/src/query-no-results/block.json b/packages/block-library/src/query-no-results/block.json index c7d3ff500e0f43..c2b7224aa40b14 100644 --- a/packages/block-library/src/query-no-results/block.json +++ b/packages/block-library/src/query-no-results/block.json @@ -8,16 +8,6 @@ "ancestor": [ "core/query" ], "textdomain": "default", "usesContext": [ "queryId", "query" ], - "example": { - "innerBlocks": [ - { - "name": "core/paragraph", - "attributes": { - "content": "No posts were found." - } - } - ] - }, "supports": { "align": true, "reusable": false, diff --git a/packages/block-library/src/query-no-results/index.js b/packages/block-library/src/query-no-results/index.js index 1c56638cdfdba8..fab5993148470e 100644 --- a/packages/block-library/src/query-no-results/index.js +++ b/packages/block-library/src/query-no-results/index.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { __ } from '@wordpress/i18n'; import { loop as icon } from '@wordpress/icons'; /** @@ -18,6 +19,16 @@ export const settings = { icon, edit, save, + example: { + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { + content: __( 'No posts were found.' ), + }, + }, + ], + }, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/query-pagination-numbers/edit.js b/packages/block-library/src/query-pagination-numbers/edit.js index b8d8c160cc874d..cf2f92f41791ff 100644 --- a/packages/block-library/src/query-pagination-numbers/edit.js +++ b/packages/block-library/src/query-pagination-numbers/edit.js @@ -3,7 +3,16 @@ */ import { __ } from '@wordpress/i18n'; import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; -import { PanelBody, RangeControl } from '@wordpress/components'; +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + RangeControl, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const createPaginationItem = ( content, Tag = 'a', extraClass = '' ) => ( <Tag key={ content } className={ `page-numbers ${ extraClass }` }> @@ -46,28 +55,41 @@ export default function QueryPaginationNumbersEdit( { const paginationNumbers = previewPaginationNumbers( parseInt( midSize, 10 ) ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + return ( <> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <RangeControl - __next40pxDefaultSize - __nextHasNoMarginBottom + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => setAttributes( { midSize: 2 } ) } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem label={ __( 'Number of links' ) } - help={ __( - 'Specify how many links can appear before and after the current page number. Links to the first, current and last page are always visible.' - ) } - value={ midSize } - onChange={ ( value ) => { - setAttributes( { - midSize: parseInt( value, 10 ), - } ); - } } - min={ 0 } - max={ 5 } - withInputField={ false } - /> - </PanelBody> + hasValue={ () => midSize !== undefined } + onDeselect={ () => setAttributes( { midSize: 2 } ) } + isShownByDefault + > + <RangeControl + __next40pxDefaultSize + __nextHasNoMarginBottom + label={ __( 'Number of links' ) } + help={ __( + 'Specify how many links can appear before and after the current page number. Links to the first, current and last page are always visible.' + ) } + value={ midSize } + onChange={ ( value ) => { + setAttributes( { + midSize: parseInt( value, 10 ), + } ); + } } + min={ 0 } + max={ 5 } + withInputField={ false } + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <div { ...useBlockProps() }>{ paginationNumbers }</div> </> diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php index 1592f0a10cbff5..20b59109874d9e 100644 --- a/packages/block-library/src/query-pagination-previous/index.php +++ b/packages/block-library/src/query-pagination-previous/index.php @@ -19,14 +19,14 @@ function render_block_core_query_pagination_previous( $attributes, $content, $block ) { $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; - - $wrapper_attributes = get_block_wrapper_attributes(); - $show_label = isset( $block->context['showLabel'] ) ? (bool) $block->context['showLabel'] : true; - $default_label = __( 'Previous Page' ); - $label_text = isset( $attributes['label'] ) && ! empty( $attributes['label'] ) ? esc_html( $attributes['label'] ) : $default_label; - $label = $show_label ? $label_text : ''; - $pagination_arrow = get_query_pagination_arrow( $block, false ); + $wrapper_attributes = get_block_wrapper_attributes(); + $show_label = isset( $block->context['showLabel'] ) ? (bool) $block->context['showLabel'] : true; + $default_label = __( 'Previous Page' ); + $label_text = isset( $attributes['label'] ) && ! empty( $attributes['label'] ) ? esc_html( $attributes['label'] ) : $default_label; + $label = $show_label ? $label_text : ''; + $pagination_arrow = get_query_pagination_arrow( $block, false ); if ( ! $label ) { $wrapper_attributes .= ' aria-label="' . $label_text . '"'; } @@ -44,13 +44,20 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl add_filter( 'previous_posts_link_attributes', $filter_link_attributes ); $content = get_previous_posts_link( $label ); remove_filter( 'previous_posts_link_attributes', $filter_link_attributes ); - } elseif ( 1 !== $page ) { - $content = sprintf( - '<a href="%1$s" %2$s>%3$s</a>', - esc_url( add_query_arg( $page_key, $page - 1 ) ), - $wrapper_attributes, - $label - ); + } else { + $block_query = new WP_Query( build_query_vars_from_query_block( $block, $page ) ); + $block_max_pages = $block_query->max_num_pages; + $total = ! $max_page || $max_page > $block_max_pages ? $block_max_pages : $max_page; + wp_reset_postdata(); + + if ( 1 < $page && $page <= $total ) { + $content = sprintf( + '<a href="%1$s" %2$s>%3$s</a>', + esc_url( add_query_arg( $page_key, $page - 1 ) ), + $wrapper_attributes, + $label + ); + } } if ( $enhanced_pagination && isset( $content ) ) { diff --git a/packages/block-library/src/query-pagination/edit.js b/packages/block-library/src/query-pagination/edit.js index e051c2e67e7e5a..8ca0705058be28 100644 --- a/packages/block-library/src/query-pagination/edit.js +++ b/packages/block-library/src/query-pagination/edit.js @@ -8,8 +8,11 @@ import { useInnerBlocksProps, store as blockEditorStore, } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; -import { PanelBody } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; import { useEffect } from '@wordpress/element'; /** @@ -17,6 +20,7 @@ import { useEffect } from '@wordpress/element'; */ import { QueryPaginationArrowControls } from './query-pagination-arrow-controls'; import { QueryPaginationLabelControl } from './query-pagination-label-control'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const TEMPLATE = [ [ 'core/query-pagination-previous' ], @@ -46,36 +50,74 @@ export default function QueryPaginationEdit( { }, [ clientId ] ); + const { __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); const blockProps = useBlockProps(); const innerBlocksProps = useInnerBlocksProps( blockProps, { template: TEMPLATE, } ); + // Always show label text if paginationArrow is set to 'none'. useEffect( () => { if ( paginationArrow === 'none' && ! showLabel ) { + __unstableMarkNextChangeAsNotPersistent(); setAttributes( { showLabel: true } ); } - }, [ paginationArrow, setAttributes, showLabel ] ); + }, [ + paginationArrow, + setAttributes, + showLabel, + __unstableMarkNextChangeAsNotPersistent, + ] ); + return ( <> { hasNextPreviousBlocks && ( <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <QueryPaginationArrowControls - value={ paginationArrow } - onChange={ ( value ) => { - setAttributes( { paginationArrow: value } ); - } } - /> - { paginationArrow !== 'none' && ( - <QueryPaginationLabelControl - value={ showLabel } + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + paginationArrow: 'none', + showLabel: true, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + hasValue={ () => paginationArrow !== 'none' } + label={ __( 'Pagination arrow' ) } + onDeselect={ () => + setAttributes( { paginationArrow: 'none' } ) + } + isShownByDefault + > + <QueryPaginationArrowControls + value={ paginationArrow } onChange={ ( value ) => { - setAttributes( { showLabel: value } ); + setAttributes( { paginationArrow: value } ); } } /> + </ToolsPanelItem> + { paginationArrow !== 'none' && ( + <ToolsPanelItem + hasValue={ () => ! showLabel } + label={ __( 'Show text' ) } + onDeselect={ () => + setAttributes( { showLabel: true } ) + } + isShownByDefault + > + <QueryPaginationLabelControl + value={ showLabel } + onChange={ ( value ) => { + setAttributes( { showLabel: value } ); + } } + /> + </ToolsPanelItem> ) } - </PanelBody> + </ToolsPanel> </InspectorControls> ) } <nav { ...innerBlocksProps } /> diff --git a/packages/block-library/src/query-pagination/query-pagination-label-control.js b/packages/block-library/src/query-pagination/query-pagination-label-control.js index 9ff80a663adeb5..16766c19bef086 100644 --- a/packages/block-library/src/query-pagination/query-pagination-label-control.js +++ b/packages/block-library/src/query-pagination/query-pagination-label-control.js @@ -9,9 +9,7 @@ export function QueryPaginationLabelControl( { value, onChange } ) { <ToggleControl __nextHasNoMarginBottom label={ __( 'Show label text' ) } - help={ __( - 'Toggle off to hide the label text, e.g. "Next Page".' - ) } + help={ __( 'Make label text visible, e.g. "Next Page".' ) } onChange={ onChange } checked={ value === true } /> diff --git a/packages/block-library/src/query-total/block.json b/packages/block-library/src/query-total/block.json index 02dbbbbb00f749..f6449fbd8ad4bd 100644 --- a/packages/block-library/src/query-total/block.json +++ b/packages/block-library/src/query-total/block.json @@ -40,6 +40,13 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true } - } + }, + "style": "wp-block-query-total" } diff --git a/packages/block-library/src/query-total/edit.js b/packages/block-library/src/query-total/edit.js index 4824021ae99b0d..d91a1990715727 100644 --- a/packages/block-library/src/query-total/edit.js +++ b/packages/block-library/src/query-total/edit.js @@ -48,27 +48,25 @@ export default function QueryTotalEdit( { attributes, setAttributes } ) { // Controls for the block. const controls = ( - <> - <BlockControls> - <ToolbarGroup> - <ToolbarDropdownMenu - icon={ getButtonPositionIcon() } - label={ __( 'Change display type' ) } - controls={ buttonPositionControls } - /> - </ToolbarGroup> - </BlockControls> - </> + <BlockControls> + <ToolbarGroup> + <ToolbarDropdownMenu + icon={ getButtonPositionIcon() } + label={ __( 'Change display type' ) } + controls={ buttonPositionControls } + /> + </ToolbarGroup> + </BlockControls> ); // Render output based on the selected display type. const renderDisplay = () => { if ( displayType === 'total-results' ) { - return <div>{ __( '12 results found' ) }</div>; + return <>{ __( '12 results found' ) }</>; } if ( displayType === 'range-display' ) { - return <div>{ __( 'Displaying 1 – 10 of 12' ) }</div>; + return <>{ __( 'Displaying 1 – 10 of 12' ) }</>; } return null; diff --git a/packages/block-library/src/query-total/index.php b/packages/block-library/src/query-total/index.php index 5a8ab76b5d1ef4..ff2ac486727b92 100644 --- a/packages/block-library/src/query-total/index.php +++ b/packages/block-library/src/query-total/index.php @@ -40,32 +40,28 @@ function render_block_core_query_total( $attributes, $content, $block ) { switch ( $attributes['displayType'] ) { case 'range-display': if ( $start === $end ) { - $range_text = sprintf( + $output = sprintf( /* translators: 1: Start index of posts, 2: Total number of posts */ __( 'Displaying %1$s of %2$s' ), - '<strong>' . $start . '</strong>', - '<strong>' . $max_rows . '</strong>' + $start, + $max_rows ); } else { - $range_text = sprintf( + $output = sprintf( /* translators: 1: Start index of posts, 2: End index of posts, 3: Total number of posts */ __( 'Displaying %1$s – %2$s of %3$s' ), - '<strong>' . $start . '</strong>', - '<strong>' . $end . '</strong>', - '<strong>' . $max_rows . '</strong>' + $start, + $end, + $max_rows ); } - $output = sprintf( '<p>%s</p>', $range_text ); break; case 'total-results': default: - $output = sprintf( - '<p><strong>%d</strong> %s</p>', - $max_rows, - _n( 'result found', 'results found', $max_rows ) - ); + // translators: %d: number of results. + $output = sprintf( _n( '%d result found', '%d results found', $max_rows ), $max_rows ); break; } diff --git a/packages/block-library/src/query-total/style.scss b/packages/block-library/src/query-total/style.scss new file mode 100644 index 00000000000000..c6a2bc131cfaf9 --- /dev/null +++ b/packages/block-library/src/query-total/style.scss @@ -0,0 +1,4 @@ +.wp-block-query-total { + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; +} diff --git a/packages/block-library/src/read-more/index.js b/packages/block-library/src/read-more/index.js index 497cd77f429e62..f982f35151b4b8 100644 --- a/packages/block-library/src/read-more/index.js +++ b/packages/block-library/src/read-more/index.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { __ } from '@wordpress/i18n'; import { link as icon } from '@wordpress/icons'; /** @@ -16,6 +17,11 @@ export { metadata, name }; export const settings = { icon, edit, + example: { + attributes: { + content: __( 'Read more' ), + }, + }, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/site-logo/block.json b/packages/block-library/src/site-logo/block.json index 3bdbdc1b809ab1..1f5b3a5525e3ec 100644 --- a/packages/block-library/src/site-logo/block.json +++ b/packages/block-library/src/site-logo/block.json @@ -12,11 +12,13 @@ }, "isLink": { "type": "boolean", - "default": true + "default": true, + "role": "content" }, "linkTarget": { "type": "string", - "default": "_self" + "default": "_self", + "role": "content" }, "shouldSyncIcon": { "type": "boolean" diff --git a/packages/block-library/src/site-title/block.json b/packages/block-library/src/site-title/block.json index c75b1bc229beb9..8edf6b945f9ce2 100644 --- a/packages/block-library/src/site-title/block.json +++ b/packages/block-library/src/site-title/block.json @@ -20,11 +20,13 @@ }, "isLink": { "type": "boolean", - "default": true + "default": true, + "role": "content" }, "linkTarget": { "type": "string", - "default": "_self" + "default": "_self", + "role": "content" } }, "example": { diff --git a/packages/block-library/src/site-title/edit.js b/packages/block-library/src/site-title/edit.js index 82e3c1d7f7bb40..0e3e96bd87cb3d 100644 --- a/packages/block-library/src/site-title/edit.js +++ b/packages/block-library/src/site-title/edit.js @@ -17,10 +17,19 @@ import { useBlockProps, HeadingLevelDropdown, } from '@wordpress/block-editor'; -import { ToggleControl, PanelBody } from '@wordpress/components'; +import { + ToggleControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + export default function SiteTitleEdit( { attributes, setAttributes, @@ -43,6 +52,7 @@ export default function SiteTitleEdit( { }; }, [] ); const { editEntityRecord } = useDispatch( coreStore ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); function setTitle( newTitle ) { editEntityRecord( 'root', 'site', undefined, { @@ -109,26 +119,53 @@ export default function SiteTitleEdit( { /> </BlockControls> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + isLink: true, + linkTarget: '_self', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + hasValue={ () => ! isLink } label={ __( 'Make title link to home' ) } - onChange={ () => setAttributes( { isLink: ! isLink } ) } - checked={ isLink } - /> - { isLink && ( + onDeselect={ () => setAttributes( { isLink: true } ) } + isShownByDefault + > <ToggleControl __nextHasNoMarginBottom - label={ __( 'Open in new tab' ) } - onChange={ ( value ) => - setAttributes( { - linkTarget: value ? '_blank' : '_self', - } ) + label={ __( 'Make title link to home' ) } + onChange={ () => + setAttributes( { isLink: ! isLink } ) } - checked={ linkTarget === '_blank' } + checked={ isLink } /> + </ToolsPanelItem> + { isLink && ( + <ToolsPanelItem + hasValue={ () => linkTarget !== '_self' } + label={ __( 'Open in new tab' ) } + onDeselect={ () => + setAttributes( { linkTarget: '_self' } ) + } + isShownByDefault + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Open in new tab' ) } + onChange={ ( value ) => + setAttributes( { + linkTarget: value ? '_blank' : '_self', + } ) + } + checked={ linkTarget === '_blank' } + /> + </ToolsPanelItem> ) } - </PanelBody> + </ToolsPanel> </InspectorControls> { siteTitleContent } </> diff --git a/packages/block-library/src/social-link/edit.js b/packages/block-library/src/social-link/edit.js index 91f1e4170b33dd..4cd24505fd552a 100644 --- a/packages/block-library/src/social-link/edit.js +++ b/packages/block-library/src/social-link/edit.js @@ -22,10 +22,10 @@ import { useState, useRef } from '@wordpress/element'; import { Button, Dropdown, - PanelBody, - PanelRow, TextControl, ToolbarButton, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper, } from '@wordpress/components'; import { useMergeRefs } from '@wordpress/compose'; @@ -36,6 +36,7 @@ import { keyboardReturn } from '@wordpress/icons'; * Internal dependencies */ import { getIconBySite, getNameBySite } from './social-list'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const SocialLinkURLPopover = ( { url, @@ -109,6 +110,7 @@ const SocialLinkEdit = ( { clientId, } ) => { const { url, service, label = '', rel } = attributes; + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); const { showLabels, iconColor, @@ -195,8 +197,21 @@ const SocialLinkEdit = ( { </BlockControls> ) } <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <PanelRow> + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { label: undefined } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + isShownByDefault + label={ __( 'Text' ) } + hasValue={ () => !! label } + onDeselect={ () => { + setAttributes( { label: undefined } ); + } } + > <TextControl __next40pxDefaultSize __nextHasNoMarginBottom @@ -210,8 +225,8 @@ const SocialLinkEdit = ( { } placeholder={ socialLinkName } /> - </PanelRow> - </PanelBody> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> <InspectorControls group="advanced"> <TextControl diff --git a/packages/block-library/src/social-links/edit.js b/packages/block-library/src/social-links/edit.js index 068b34a3a70a4e..72fd265d629fb7 100644 --- a/packages/block-library/src/social-links/edit.js +++ b/packages/block-library/src/social-links/edit.js @@ -22,14 +22,20 @@ import { import { MenuGroup, MenuItem, - PanelBody, ToggleControl, ToolbarDropdownMenu, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { check } from '@wordpress/icons'; import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + const sizeOptions = [ { name: __( 'Small' ), value: 'has-small-icon-size' }, { name: __( 'Normal' ), value: 'has-normal-icon-size' }, @@ -68,6 +74,8 @@ export function SocialLinksEdit( props ) { const logosOnly = attributes.className?.includes( 'is-style-logos-only' ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + // Remove icon background color when logos only style is selected or // restore it when any other style is selected. const backgroundBackupRef = useRef( {} ); @@ -198,24 +206,53 @@ export function SocialLinksEdit( props ) { </ToolbarDropdownMenu> </BlockControls> <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + openInNewTab: false, + showLabels: false, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + isShownByDefault label={ __( 'Open links in new tab' ) } - checked={ openInNewTab } - onChange={ () => - setAttributes( { openInNewTab: ! openInNewTab } ) + hasValue={ () => !! openInNewTab } + onDeselect={ () => + setAttributes( { openInNewTab: false } ) } - /> - <ToggleControl - __nextHasNoMarginBottom + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Open links in new tab' ) } + checked={ openInNewTab } + onChange={ () => + setAttributes( { + openInNewTab: ! openInNewTab, + } ) + } + /> + </ToolsPanelItem> + <ToolsPanelItem + isShownByDefault label={ __( 'Show text' ) } - checked={ showLabels } - onChange={ () => - setAttributes( { showLabels: ! showLabels } ) + hasValue={ () => !! showLabels } + onDeselect={ () => + setAttributes( { showLabels: false } ) } - /> - </PanelBody> + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Show text' ) } + checked={ showLabels } + onChange={ () => + setAttributes( { showLabels: ! showLabels } ) + } + /> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> { colorGradientSettings.hasColorsOrGradients && ( <InspectorControls group="color"> diff --git a/packages/block-library/src/spacer/controls.js b/packages/block-library/src/spacer/controls.js index 1e899e15aff0de..fde06d3ee8c339 100644 --- a/packages/block-library/src/spacer/controls.js +++ b/packages/block-library/src/spacer/controls.js @@ -10,10 +10,11 @@ import { privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { - PanelBody, __experimentalUseCustomUnits as useCustomUnits, __experimentalUnitControl as UnitControl, __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; import { View } from '@wordpress/primitives'; @@ -94,28 +95,54 @@ export default function SpacerControls( { } ) { return ( <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + width: undefined, + height: '100px', + } ); + } } + > { orientation === 'horizontal' && ( - <DimensionInput + <ToolsPanelItem label={ __( 'Width' ) } - value={ width } - onChange={ ( nextWidth ) => - setAttributes( { width: nextWidth } ) + isShownByDefault + hasValue={ () => width !== undefined } + onDeselect={ () => + setAttributes( { width: undefined } ) } - isResizing={ isResizing } - /> + > + <DimensionInput + label={ __( 'Width' ) } + value={ width } + onChange={ ( nextWidth ) => + setAttributes( { width: nextWidth } ) + } + isResizing={ isResizing } + /> + </ToolsPanelItem> ) } { orientation !== 'horizontal' && ( - <DimensionInput + <ToolsPanelItem label={ __( 'Height' ) } - value={ height } - onChange={ ( nextHeight ) => - setAttributes( { height: nextHeight } ) + isShownByDefault + hasValue={ () => height !== '100px' } + onDeselect={ () => + setAttributes( { height: '100px' } ) } - isResizing={ isResizing } - /> + > + <DimensionInput + label={ __( 'Height' ) } + value={ height } + onChange={ ( nextHeight ) => + setAttributes( { height: nextHeight } ) + } + isResizing={ isResizing } + /> + </ToolsPanelItem> ) } - </PanelBody> + </ToolsPanel> </InspectorControls> ); } diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index a8819c2084dc2e..c61049c23151b9 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -37,6 +37,7 @@ @import "./post-author-biography/style.scss"; @import "./post-comments-form/style.scss"; @import "./post-content/style.scss"; +@import "./post-comments-link/style.scss"; @import "./post-date/style.scss"; @import "./post-excerpt/style.scss"; @import "./post-featured-image/style.scss"; @@ -50,6 +51,7 @@ @import "./post-template/style.scss"; @import "./query-pagination/style.scss"; @import "./query-title/style.scss"; +@import "./query-total/style.scss"; @import "./quote/style.scss"; @import "./read-more/style.scss"; @import "./rss/style.scss"; diff --git a/packages/block-library/src/table-of-contents/block.json b/packages/block-library/src/table-of-contents/block.json index 5eb6e729d3f03e..68266166080bbd 100644 --- a/packages/block-library/src/table-of-contents/block.json +++ b/packages/block-library/src/table-of-contents/block.json @@ -62,57 +62,5 @@ } } }, - "example": { - "innerBlocks": [ - { - "name": "core/heading", - "attributes": { - "level": 2, - "content": "Heading" - } - }, - { - "name": "core/heading", - "attributes": { - "level": 3, - "content": "Subheading" - } - }, - { - "name": "core/heading", - "attributes": { - "level": 2, - "content": "Heading" - } - }, - { - "name": "core/heading", - "attributes": { - "level": 3, - "content": "Subheading" - } - } - ], - "attributes": { - "headings": [ - { - "content": "Heading", - "level": 2 - }, - { - "content": "Subheading", - "level": 3 - }, - { - "content": "Heading", - "level": 2 - }, - { - "content": "Subheading", - "level": 3 - } - ] - } - }, "style": "wp-block-table-of-contents" } diff --git a/packages/block-library/src/table-of-contents/edit.js b/packages/block-library/src/table-of-contents/edit.js index c95b89200cb88c..394ff2666067d4 100644 --- a/packages/block-library/src/table-of-contents/edit.js +++ b/packages/block-library/src/table-of-contents/edit.js @@ -122,7 +122,7 @@ export default function TableOfContentsEdit( { 'Only including headings from the current page (if the post is paginated).' ) : __( - 'Toggle to only include headings from the current page (if the post is paginated).' + 'Include headings from all pages (if the post is paginated).' ) } /> diff --git a/packages/block-library/src/table-of-contents/index.js b/packages/block-library/src/table-of-contents/index.js index 408538a7dcadbd..ff1b658966f19f 100644 --- a/packages/block-library/src/table-of-contents/index.js +++ b/packages/block-library/src/table-of-contents/index.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { __ } from '@wordpress/i18n'; import { tableOfContents as icon } from '@wordpress/icons'; /** @@ -19,6 +20,58 @@ export const settings = { icon, edit, save, + example: { + innerBlocks: [ + { + name: 'core/heading', + attributes: { + level: 2, + content: __( 'Heading' ), + }, + }, + { + name: 'core/heading', + attributes: { + level: 3, + content: __( 'Subheading' ), + }, + }, + { + name: 'core/heading', + attributes: { + level: 2, + content: __( 'Heading' ), + }, + }, + { + name: 'core/heading', + attributes: { + level: 3, + content: __( 'Subheading' ), + }, + }, + ], + attributes: { + headings: [ + { + content: __( 'Heading' ), + level: 2, + }, + { + content: __( 'Subheading' ), + level: 3, + }, + { + content: __( 'Heading' ), + level: 2, + }, + { + content: __( 'Subheading' ), + level: 3, + }, + ], + }, + }, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/table/block.json b/packages/block-library/src/table/block.json index 11dd5b5f323e3b..2f0ea753f6f8de 100644 --- a/packages/block-library/src/table/block.json +++ b/packages/block-library/src/table/block.json @@ -195,11 +195,14 @@ "width": true } }, - "__experimentalSelector": ".wp-block-table > table", "interactivity": { "clientNavigation": true } }, + "selectors": { + "root": ".wp-block-table > table", + "spacing": ".wp-block-table" + }, "styles": [ { "name": "regular", diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index f1cb3fa5d8b8ae..a6c8be3c4a4899 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -20,12 +20,13 @@ import { import { __ } from '@wordpress/i18n'; import { Button, - PanelBody, Placeholder, TextControl, ToggleControl, ToolbarDropdownMenu, __experimentalHasSplitBorders as hasSplitBorders, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { alignLeft, @@ -56,6 +57,7 @@ import { isEmptyTableSection, } from './state'; import { Caption } from '../utils/caption'; +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; const ALIGNMENT_CONTROLS = [ { @@ -108,6 +110,8 @@ function TableEdit( { const tableRef = useRef(); const [ hasTableCreated, setHasTableCreated ] = useState( false ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + /** * Updates the initial column count used for table creation. * @@ -473,33 +477,67 @@ function TableEdit( { </> ) } <InspectorControls> - <PanelBody - title={ __( 'Settings' ) } - className="blocks-table-settings" + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + hasFixedLayout: true, + head: [], + foot: [], + } ); + } } + dropdownMenuProps={ dropdownMenuProps } > - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanelItem + hasValue={ () => hasFixedLayout !== true } label={ __( 'Fixed width table cells' ) } - checked={ !! hasFixedLayout } - onChange={ onChangeFixedLayout } - /> + onDeselect={ () => + setAttributes( { hasFixedLayout: true } ) + } + isShownByDefault + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Fixed width table cells' ) } + checked={ !! hasFixedLayout } + onChange={ onChangeFixedLayout } + /> + </ToolsPanelItem> { ! isEmpty && ( <> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanelItem + hasValue={ () => head && head.length } label={ __( 'Header section' ) } - checked={ !! ( head && head.length ) } - onChange={ onToggleHeaderSection } - /> - <ToggleControl - __nextHasNoMarginBottom + onDeselect={ () => + setAttributes( { head: [] } ) + } + isShownByDefault + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Header section' ) } + checked={ !! ( head && head.length ) } + onChange={ onToggleHeaderSection } + /> + </ToolsPanelItem> + <ToolsPanelItem + hasValue={ () => foot && foot.length } label={ __( 'Footer section' ) } - checked={ !! ( foot && foot.length ) } - onChange={ onToggleFooterSection } - /> + onDeselect={ () => + setAttributes( { foot: [] } ) + } + isShownByDefault + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Footer section' ) } + checked={ !! ( foot && foot.length ) } + onChange={ onToggleFooterSection } + /> + </ToolsPanelItem> </> ) } - </PanelBody> + </ToolsPanel> </InspectorControls> { ! isEmpty && ( <table diff --git a/packages/block-library/src/tag-cloud/edit.js b/packages/block-library/src/tag-cloud/edit.js index eeb568e7a89ef1..7e544d2474f049 100644 --- a/packages/block-library/src/tag-cloud/edit.js +++ b/packages/block-library/src/tag-cloud/edit.js @@ -4,14 +4,14 @@ import { Flex, FlexItem, - PanelBody, ToggleControl, SelectControl, RangeControl, __experimentalUnitControl as UnitControl, __experimentalUseCustomUnits as useCustomUnits, __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue, - __experimentalVStack as VStack, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, Disabled, } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; @@ -24,6 +24,11 @@ import { import ServerSideRender from '@wordpress/server-side-render'; import { store as coreStore } from '@wordpress/core-data'; +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; + /** * Minimum number of tags a user can show using this block. * @@ -51,6 +56,7 @@ function TagCloudEdit( { attributes, setAttributes } ) { } = attributes; const [ availableUnits ] = useSettings( 'spacing.units' ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); // The `pt` unit is used as the default value and is therefore // always considered an available unit. @@ -118,10 +124,26 @@ function TagCloudEdit( { attributes, setAttributes } ) { const inspectorControls = ( <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - <VStack - spacing={ 4 } - className="wp-block-tag-cloud__inspector-settings" + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + taxonomy: 'post_tag', + showTagCounts: false, + numberOfTags: 45, + smallestFontSize: '8pt', + largestFontSize: '22pt', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + <ToolsPanelItem + hasValue={ () => taxonomy !== 'post_tag' } + label={ __( 'Taxonomy' ) } + onDeselect={ () => + setAttributes( { taxonomy: 'post_tag' } ) + } + isShownByDefault > <SelectControl __nextHasNoMarginBottom @@ -133,6 +155,20 @@ function TagCloudEdit( { attributes, setAttributes } ) { setAttributes( { taxonomy: selectedTaxonomy } ) } /> + </ToolsPanelItem> + <ToolsPanelItem + hasValue={ () => + smallestFontSize !== '8pt' || largestFontSize !== '22pt' + } + label={ __( 'Font size' ) } + onDeselect={ () => + setAttributes( { + smallestFontSize: '8pt', + largestFontSize: '22pt', + } ) + } + isShownByDefault + > <Flex gap={ 4 }> <FlexItem isBlock> <UnitControl @@ -167,6 +203,13 @@ function TagCloudEdit( { attributes, setAttributes } ) { /> </FlexItem> </Flex> + </ToolsPanelItem> + <ToolsPanelItem + hasValue={ () => numberOfTags !== 45 } + label={ __( 'Number of tags' ) } + onDeselect={ () => setAttributes( { numberOfTags: 45 } ) } + isShownByDefault + > <RangeControl __nextHasNoMarginBottom __next40pxDefaultSize @@ -179,6 +222,15 @@ function TagCloudEdit( { attributes, setAttributes } ) { max={ MAX_TAGS } required /> + </ToolsPanelItem> + <ToolsPanelItem + hasValue={ () => showTagCounts !== false } + label={ __( 'Show tag counts' ) } + onDeselect={ () => + setAttributes( { showTagCounts: false } ) + } + isShownByDefault + > <ToggleControl __nextHasNoMarginBottom label={ __( 'Show tag counts' ) } @@ -187,8 +239,8 @@ function TagCloudEdit( { attributes, setAttributes } ) { setAttributes( { showTagCounts: ! showTagCounts } ) } /> - </VStack> - </PanelBody> + </ToolsPanelItem> + </ToolsPanel> </InspectorControls> ); diff --git a/packages/block-library/src/tag-cloud/editor.scss b/packages/block-library/src/tag-cloud/editor.scss index e85129e22f1aca..d00a450174f2fd 100644 --- a/packages/block-library/src/tag-cloud/editor.scss +++ b/packages/block-library/src/tag-cloud/editor.scss @@ -9,11 +9,3 @@ border: none; border-radius: inherit; } - -.wp-block-tag-cloud__inspector-settings { - .components-base-control, - .components-base-control:last-child { - // Cancel out extra margins added by block inspector - margin-bottom: 0; - } -} diff --git a/packages/block-library/src/video/edit-common-settings.js b/packages/block-library/src/video/edit-common-settings.js index 9394bfaf5c6145..4f85f929b07cfc 100644 --- a/packages/block-library/src/video/edit-common-settings.js +++ b/packages/block-library/src/video/edit-common-settings.js @@ -2,7 +2,11 @@ * WordPress dependencies */ import { __, _x } from '@wordpress/i18n'; -import { ToggleControl, SelectControl } from '@wordpress/components'; +import { + ToggleControl, + SelectControl, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; import { useMemo, useCallback, Platform } from '@wordpress/element'; const options = [ @@ -47,50 +51,104 @@ const VideoSettings = ( { setAttributes, attributes } ) => { return ( <> - <ToggleControl - __nextHasNoMarginBottom + <ToolsPanelItem label={ __( 'Autoplay' ) } - onChange={ toggleFactory.autoplay } - checked={ !! autoplay } - help={ getAutoplayHelp } - /> - <ToggleControl - __nextHasNoMarginBottom + isShownByDefault + hasValue={ () => !! autoplay } + onDeselect={ () => { + setAttributes( { autoplay: false } ); + } } + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Autoplay' ) } + onChange={ toggleFactory.autoplay } + checked={ !! autoplay } + help={ getAutoplayHelp } + /> + </ToolsPanelItem> + <ToolsPanelItem label={ __( 'Loop' ) } - onChange={ toggleFactory.loop } - checked={ !! loop } - /> - <ToggleControl - __nextHasNoMarginBottom + isShownByDefault + hasValue={ () => !! loop } + onDeselect={ () => { + setAttributes( { loop: false } ); + } } + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Loop' ) } + onChange={ toggleFactory.loop } + checked={ !! loop } + /> + </ToolsPanelItem> + <ToolsPanelItem label={ __( 'Muted' ) } - onChange={ toggleFactory.muted } - checked={ !! muted } - /> - <ToggleControl - __nextHasNoMarginBottom + isShownByDefault + hasValue={ () => !! muted } + onDeselect={ () => { + setAttributes( { muted: false } ); + } } + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Muted' ) } + onChange={ toggleFactory.muted } + checked={ !! muted } + /> + </ToolsPanelItem> + <ToolsPanelItem label={ __( 'Playback controls' ) } - onChange={ toggleFactory.controls } - checked={ !! controls } - /> - <ToggleControl - __nextHasNoMarginBottom - /* translators: Setting to play videos within the webpage on mobile browsers rather than opening in a fullscreen player. */ + isShownByDefault + hasValue={ () => ! controls } + onDeselect={ () => { + setAttributes( { controls: true } ); + } } + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Playback controls' ) } + onChange={ toggleFactory.controls } + checked={ !! controls } + /> + </ToolsPanelItem> + <ToolsPanelItem label={ __( 'Play inline' ) } - onChange={ toggleFactory.playsInline } - checked={ !! playsInline } - help={ __( - 'When enabled, videos will play directly within the webpage on mobile browsers, instead of opening in a fullscreen player.' - ) } - /> - <SelectControl - __next40pxDefaultSize - __nextHasNoMarginBottom + isShownByDefault + hasValue={ () => !! playsInline } + onDeselect={ () => { + setAttributes( { playsInline: false } ); + } } + > + <ToggleControl + __nextHasNoMarginBottom + /* translators: Setting to play videos within the webpage on mobile browsers rather than opening in a fullscreen player. */ + label={ __( 'Play inline' ) } + onChange={ toggleFactory.playsInline } + checked={ playsInline } + help={ __( + 'When enabled, videos will play directly within the webpage on mobile browsers, instead of opening in a fullscreen player.' + ) } + /> + </ToolsPanelItem> + <ToolsPanelItem label={ __( 'Preload' ) } - value={ preload } - onChange={ onChangePreload } - options={ options } - hideCancelButton - /> + isShownByDefault + hasValue={ () => preload !== 'metadata' } + onDeselect={ () => { + setAttributes( { preload: 'metadata' } ); + } } + > + <SelectControl + __next40pxDefaultSize + __nextHasNoMarginBottom + label={ __( 'Preload' ) } + value={ preload } + onChange={ onChangePreload } + options={ options } + hideCancelButton + /> + </ToolsPanelItem> </> ); }; diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js index 32221919c7ea20..95ecab25f95985 100644 --- a/packages/block-library/src/video/edit.js +++ b/packages/block-library/src/video/edit.js @@ -8,25 +8,21 @@ import clsx from 'clsx'; */ import { isBlobURL } from '@wordpress/blob'; import { - BaseControl, - Button, Disabled, - PanelBody, Spinner, Placeholder, + __experimentalToolsPanel as ToolsPanel, } from '@wordpress/components'; import { BlockControls, BlockIcon, InspectorControls, MediaPlaceholder, - MediaUpload, - MediaUploadCheck, MediaReplaceFlow, useBlockProps, } from '@wordpress/block-editor'; import { useRef, useEffect, useState } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { useInstanceId } from '@wordpress/compose'; import { useDispatch } from '@wordpress/data'; import { video as icon } from '@wordpress/icons'; @@ -35,15 +31,18 @@ import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ +import PosterImage from './poster-image'; import { createUpgradedEmbedBlock } from '../embed/util'; -import { useUploadMediaFromBlobURL } from '../utils/hooks'; +import { + useUploadMediaFromBlobURL, + useToolsPanelDropdownMenuProps, +} from '../utils/hooks'; import VideoCommonSettings from './edit-common-settings'; import TracksEditor from './tracks-editor'; import Tracks from './tracks'; import { Caption } from '../utils/caption'; const ALLOWED_MEDIA_TYPES = [ 'video' ]; -const VIDEO_POSTER_ALLOWED_MEDIA_TYPES = [ 'image' ]; function VideoEdit( { isSelected: isSingleSelected, @@ -55,9 +54,9 @@ function VideoEdit( { } ) { const instanceId = useInstanceId( VideoEdit ); const videoPlayer = useRef(); - const posterImageButton = useRef(); const { id, controls, poster, src, tracks } = attributes; const [ temporaryURL, setTemporaryURL ] = useState( attributes.blob ); + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); useUploadMediaFromBlobURL( { url: temporaryURL, @@ -174,19 +173,6 @@ function VideoEdit( { ); } - function onSelectPoster( image ) { - setAttributes( { poster: image.url } ); - } - - function onRemovePoster() { - setAttributes( { poster: undefined } ); - - // Move focus back to the Media Upload button. - posterImageButton.current.focus(); - } - - const videoPosterDescription = `video-block__poster-image-description-${ instanceId }`; - return ( <> { isSingleSelected && ( @@ -214,63 +200,31 @@ function VideoEdit( { </> ) } <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ () => { + setAttributes( { + autoplay: false, + controls: true, + loop: false, + muted: false, + playsInline: false, + preload: 'metadata', + poster: '', + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > <VideoCommonSettings setAttributes={ setAttributes } attributes={ attributes } /> - <MediaUploadCheck> - <div className="editor-video-poster-control"> - <BaseControl.VisualLabel> - { __( 'Poster image' ) } - </BaseControl.VisualLabel> - <MediaUpload - title={ __( 'Select poster image' ) } - onSelect={ onSelectPoster } - allowedTypes={ - VIDEO_POSTER_ALLOWED_MEDIA_TYPES - } - render={ ( { open } ) => ( - <Button - __next40pxDefaultSize - variant="primary" - onClick={ open } - ref={ posterImageButton } - aria-describedby={ - videoPosterDescription - } - > - { ! poster - ? __( 'Select' ) - : __( 'Replace' ) } - </Button> - ) } - /> - <p id={ videoPosterDescription } hidden> - { poster - ? sprintf( - /* translators: %s: poster image URL. */ - __( - 'The current poster image url is %s' - ), - poster - ) - : __( - 'There is no poster image currently selected' - ) } - </p> - { !! poster && ( - <Button - __next40pxDefaultSize - onClick={ onRemovePoster } - variant="tertiary" - > - { __( 'Remove' ) } - </Button> - ) } - </div> - </MediaUploadCheck> - </PanelBody> + <PosterImage + poster={ poster } + setAttributes={ setAttributes } + instanceId={ instanceId } + /> + </ToolsPanel> </InspectorControls> <figure { ...blockProps }> { /* diff --git a/packages/block-library/src/video/poster-image.js b/packages/block-library/src/video/poster-image.js new file mode 100644 index 00000000000000..cde95f974d8e69 --- /dev/null +++ b/packages/block-library/src/video/poster-image.js @@ -0,0 +1,86 @@ +/** + * WordPress dependencies + */ +import { MediaUpload, MediaUploadCheck } from '@wordpress/block-editor'; +import { + Button, + BaseControl, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useRef } from '@wordpress/element'; + +function PosterImage( { poster, setAttributes, instanceId } ) { + const posterImageButton = useRef(); + const VIDEO_POSTER_ALLOWED_MEDIA_TYPES = [ 'image' ]; + + const videoPosterDescription = `video-block__poster-image-description-${ instanceId }`; + + function onSelectPoster( image ) { + setAttributes( { poster: image.url } ); + } + + function onRemovePoster() { + setAttributes( { poster: undefined } ); + + // Move focus back to the Media Upload button. + posterImageButton.current.focus(); + } + + return ( + <ToolsPanelItem + label={ __( 'Poster image' ) } + isShownByDefault + hasValue={ () => !! poster } + onDeselect={ () => { + setAttributes( { poster: '' } ); + } } + > + <MediaUploadCheck> + <div className="editor-video-poster-control"> + <BaseControl.VisualLabel> + { __( 'Poster image' ) } + </BaseControl.VisualLabel> + <MediaUpload + title={ __( 'Select poster image' ) } + onSelect={ onSelectPoster } + allowedTypes={ VIDEO_POSTER_ALLOWED_MEDIA_TYPES } + render={ ( { open } ) => ( + <Button + __next40pxDefaultSize + variant="primary" + onClick={ open } + ref={ posterImageButton } + aria-describedby={ videoPosterDescription } + > + { ! poster ? __( 'Select' ) : __( 'Replace' ) } + </Button> + ) } + /> + <p id={ videoPosterDescription } hidden> + { poster + ? sprintf( + /* translators: %s: poster image URL. */ + __( 'The current poster image url is %s' ), + poster + ) + : __( + 'There is no poster image currently selected' + ) } + </p> + { !! poster && ( + <Button + __next40pxDefaultSize + onClick={ onRemovePoster } + variant="tertiary" + > + { __( 'Remove' ) } + </Button> + ) } + </div> + </MediaUploadCheck> + </ToolsPanelItem> + ); +} + +export default PosterImage; diff --git a/packages/block-library/src/video/tracks-editor.js b/packages/block-library/src/video/tracks-editor.js index 33036a14f1fec7..a0152885f55671 100644 --- a/packages/block-library/src/video/tracks-editor.js +++ b/packages/block-library/src/video/tracks-editor.js @@ -323,7 +323,7 @@ export default function TracksEditor( { tracks = [], onChange } ) { openFileDialog(); } } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } </MenuItem> ); } } diff --git a/packages/block-library/tsconfig.json b/packages/block-library/tsconfig.json index 2a2cb1653d4285..a8423ee4a27093 100644 --- a/packages/block-library/tsconfig.json +++ b/packages/block-library/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ], "strictNullChecks": true }, diff --git a/packages/block-serialization-default-parser/tsconfig.json b/packages/block-serialization-default-parser/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/block-serialization-default-parser/tsconfig.json +++ b/packages/block-serialization-default-parser/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index aaf6558c47bada..620dfcbb8599c0 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -58,12 +58,12 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, borderColor: { value: [ 'border', 'color' ], - support: [ 'border', 'color' ], + support: [ '__experimentalBorder', 'color' ], useEngine: true, }, borderRadius: { value: [ 'border', 'radius' ], - support: [ 'border', 'radius' ], + support: [ '__experimentalBorder', 'radius' ], properties: { borderTopLeftRadius: 'topLeft', borderTopRightRadius: 'topRight', @@ -74,72 +74,72 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, borderStyle: { value: [ 'border', 'style' ], - support: [ 'border', 'style' ], + support: [ '__experimentalBorder', 'style' ], useEngine: true, }, borderWidth: { value: [ 'border', 'width' ], - support: [ 'border', 'width' ], + support: [ '__experimentalBorder', 'width' ], useEngine: true, }, borderTopColor: { value: [ 'border', 'top', 'color' ], - support: [ 'border', 'color' ], + support: [ '__experimentalBorder', 'color' ], useEngine: true, }, borderTopStyle: { value: [ 'border', 'top', 'style' ], - support: [ 'border', 'style' ], + support: [ '__experimentalBorder', 'style' ], useEngine: true, }, borderTopWidth: { value: [ 'border', 'top', 'width' ], - support: [ 'border', 'width' ], + support: [ '__experimentalBorder', 'width' ], useEngine: true, }, borderRightColor: { value: [ 'border', 'right', 'color' ], - support: [ 'border', 'color' ], + support: [ '__experimentalBorder', 'color' ], useEngine: true, }, borderRightStyle: { value: [ 'border', 'right', 'style' ], - support: [ 'border', 'style' ], + support: [ '__experimentalBorder', 'style' ], useEngine: true, }, borderRightWidth: { value: [ 'border', 'right', 'width' ], - support: [ 'border', 'width' ], + support: [ '__experimentalBorder', 'width' ], useEngine: true, }, borderBottomColor: { value: [ 'border', 'bottom', 'color' ], - support: [ 'border', 'color' ], + support: [ '__experimentalBorder', 'color' ], useEngine: true, }, borderBottomStyle: { value: [ 'border', 'bottom', 'style' ], - support: [ 'border', 'style' ], + support: [ '__experimentalBorder', 'style' ], useEngine: true, }, borderBottomWidth: { value: [ 'border', 'bottom', 'width' ], - support: [ 'border', 'width' ], + support: [ '__experimentalBorder', 'width' ], useEngine: true, }, borderLeftColor: { value: [ 'border', 'left', 'color' ], - support: [ 'border', 'color' ], + support: [ '__experimentalBorder', 'color' ], useEngine: true, }, borderLeftStyle: { value: [ 'border', 'left', 'style' ], - support: [ 'border', 'style' ], + support: [ '__experimentalBorder', 'style' ], useEngine: true, }, borderLeftWidth: { value: [ 'border', 'left', 'width' ], - support: [ 'border', 'width' ], + support: [ '__experimentalBorder', 'width' ], useEngine: true, }, color: { @@ -183,7 +183,7 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, fontFamily: { value: [ 'typography', 'fontFamily' ], - support: [ 'typography', 'fontFamily' ], + support: [ 'typography', '__experimentalFontFamily' ], useEngine: true, }, fontSize: { @@ -193,12 +193,12 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, fontStyle: { value: [ 'typography', 'fontStyle' ], - support: [ 'typography', 'fontStyle' ], + support: [ 'typography', '__experimentalFontStyle' ], useEngine: true, }, fontWeight: { value: [ 'typography', 'fontWeight' ], - support: [ 'typography', 'fontWeight' ], + support: [ 'typography', '__experimentalFontWeight' ], useEngine: true, }, lineHeight: { @@ -240,17 +240,17 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, textDecoration: { value: [ 'typography', 'textDecoration' ], - support: [ 'typography', 'textDecoration' ], + support: [ 'typography', '__experimentalTextDecoration' ], useEngine: true, }, textTransform: { value: [ 'typography', 'textTransform' ], - support: [ 'typography', 'textTransform' ], + support: [ 'typography', '__experimentalTextTransform' ], useEngine: true, }, letterSpacing: { value: [ 'typography', 'letterSpacing' ], - support: [ 'typography', 'letterSpacing' ], + support: [ 'typography', '__experimentalLetterSpacing' ], useEngine: true, }, writingMode: { @@ -297,23 +297,3 @@ export const __EXPERIMENTAL_PATHS_WITH_OVERRIDE = { 'typography.fontSizes': true, 'spacing.spacingSizes': true, }; - -export const EXPERIMENTAL_SUPPORTS_MAP = { - __experimentalBorder: 'border', -}; - -export const COMMON_EXPERIMENTAL_PROPERTIES = { - __experimentalDefaultControls: 'defaultControls', - __experimentalSkipSerialization: 'skipSerialization', -}; - -export const EXPERIMENTAL_SUPPORT_PROPERTIES = { - typography: { - __experimentalFontFamily: 'fontFamily', - __experimentalFontStyle: 'fontStyle', - __experimentalFontWeight: 'fontWeight', - __experimentalLetterSpacing: 'letterSpacing', - __experimentalTextDecoration: 'textDecoration', - __experimentalTextTransform: 'textTransform', - }, -}; diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index 25bf64ca65dc90..5eacf96fb1e5b5 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -17,6 +17,7 @@ import { getGroupingBlockName, } from './registration'; import { + isBlockRegistered, normalizeBlockType, __experimentalSanitizeBlockAttributes, } from './utils'; @@ -31,6 +32,14 @@ import { * @return {Object} Block object. */ export function createBlock( name, attributes = {}, innerBlocks = [] ) { + if ( ! isBlockRegistered( name ) ) { + return createBlock( 'core/missing', { + originalName: name, + originalContent: '', + originalUndelimitedContent: '', + } ); + } + const sanitizedAttributes = __experimentalSanitizeBlockAttributes( name, attributes @@ -94,15 +103,22 @@ export function __experimentalCloneSanitizedBlock( mergeAttributes = {}, newInnerBlocks ) { + const { name } = block; + + if ( ! isBlockRegistered( name ) ) { + return createBlock( 'core/missing', { + originalName: name, + originalContent: '', + originalUndelimitedContent: '', + } ); + } + const clientId = uuid(); - const sanitizedAttributes = __experimentalSanitizeBlockAttributes( - block.name, - { - ...block.attributes, - ...mergeAttributes, - } - ); + const sanitizedAttributes = __experimentalSanitizeBlockAttributes( name, { + ...block.attributes, + ...mergeAttributes, + } ); return { ...block, @@ -583,20 +599,11 @@ export function switchToBlockType( blocks, name ) { * * @return {Object} block. */ -export const getBlockFromExample = ( name, example ) => { - try { - return createBlock( - name, - example.attributes, - ( example.innerBlocks ?? [] ).map( ( innerBlock ) => - getBlockFromExample( innerBlock.name, innerBlock ) - ) - ); - } catch { - return createBlock( 'core/missing', { - originalName: name, - originalContent: '', - originalUndelimitedContent: '', - } ); - } -}; +export const getBlockFromExample = ( name, example ) => + createBlock( + name, + example.attributes, + ( example.innerBlocks ?? [] ).map( ( innerBlock ) => + getBlockFromExample( innerBlock.name, innerBlock ) + ) + ); diff --git a/packages/blocks/src/api/templates.js b/packages/blocks/src/api/templates.js index 71231121362a49..6f7e13f27ebe80 100644 --- a/packages/blocks/src/api/templates.js +++ b/packages/blocks/src/api/templates.js @@ -109,23 +109,12 @@ export function synchronizeBlocksWithTemplate( blocks = [], template ) { attributes ); - let [ blockName, blockAttributes ] = + const [ blockName, blockAttributes ] = convertLegacyBlockNameAndAttributes( name, normalizedAttributes ); - // If a Block is undefined at this point, use the core/missing block as - // a placeholder for a better user experience. - if ( undefined === getBlockType( blockName ) ) { - blockAttributes = { - originalName: name, - originalContent: '', - originalUndelimitedContent: '', - }; - blockName = 'core/missing'; - } - return createBlock( blockName, blockAttributes, diff --git a/packages/blocks/src/api/test/utils.js b/packages/blocks/src/api/test/utils.js index 548bbb27da3889..b1906b65b4208f 100644 --- a/packages/blocks/src/api/test/utils.js +++ b/packages/blocks/src/api/test/utils.js @@ -12,6 +12,7 @@ import { isUnmodifiedDefaultBlock, getAccessibleBlockLabel, getBlockLabel, + isBlockRegistered, __experimentalSanitizeBlockAttributes, getBlockAttributesNamesByRole, isContentBlock, @@ -213,6 +214,20 @@ describe( 'getAccessibleBlockLabel', () => { } ); } ); +describe( 'isBlockRegistered', () => { + it( 'returns true if the block is registered', () => { + registerBlockType( 'core/test-block', { title: 'Test block' } ); + expect( isBlockRegistered( 'core/test-block' ) ).toBe( true ); + unregisterBlockType( 'core/test-block' ); + } ); + + it( 'returns false if the block is not registered', () => { + expect( isBlockRegistered( 'core/not-registered-test-block' ) ).toBe( + false + ); + } ); +} ); + describe( 'sanitizeBlockAttributes', () => { afterEach( () => { getBlockTypes().forEach( ( block ) => { diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 1a215036496559..ad94d9d5c9e0c1 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -266,6 +266,17 @@ export function getDefault( attributeSchema ) { } } +/** + * Check if a block is registered. + * + * @param {string} name The block's name. + * + * @return {boolean} Whether the block is registered. + */ +export function isBlockRegistered( name ) { + return getBlockType( name ) !== undefined; +} + /** * Ensure attributes contains only values defined by block type, and merge * default values for missing attributes. @@ -370,9 +381,21 @@ export const __experimentalGetBlockAttributesNamesByRole = ( ...args ) => { return getBlockAttributesNamesByRole( ...args ); }; +/** + * Checks if a block is a content block by examining its attributes. + * A block is considered a content block if it has at least one attribute + * with a role of 'content'. + * + * @param {string} name The name of the block to check. + * @return {boolean} Whether the block is a content block. + */ export function isContentBlock( name ) { const attributes = getBlockType( name )?.attributes; + if ( ! attributes ) { + return false; + } + return !! Object.keys( attributes )?.some( ( attributeKey ) => { const attribute = attributes[ attributeKey ]; return ( diff --git a/packages/blocks/src/store/process-block-type.js b/packages/blocks/src/store/process-block-type.js index 0ca28a3c3e2070..bc7b1a0e10e774 100644 --- a/packages/blocks/src/store/process-block-type.js +++ b/packages/blocks/src/store/process-block-type.js @@ -15,13 +15,7 @@ import warning from '@wordpress/warning'; * Internal dependencies */ import { isValidIcon, normalizeIconObject, omit } from '../api/utils'; -import { - BLOCK_ICON_DEFAULT, - DEPRECATED_ENTRY_KEYS, - EXPERIMENTAL_SUPPORTS_MAP, - COMMON_EXPERIMENTAL_PROPERTIES, - EXPERIMENTAL_SUPPORT_PROPERTIES, -} from '../api/constants'; +import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from '../api/constants'; /** @typedef {import('../api/registration').WPBlockType} WPBlockType */ @@ -68,155 +62,6 @@ function mergeBlockVariations( return result; } -/** - * Stabilizes a block support configuration by converting experimental properties - * to their stable equivalents. - * - * @param {Object} unstableConfig The support configuration to stabilize. - * @param {string} stableSupportKey The stable support key for looking up properties. - * @return {Object} The stabilized support configuration. - */ -function stabilizeSupportConfig( unstableConfig, stableSupportKey ) { - const stableConfig = {}; - for ( const [ key, value ] of Object.entries( unstableConfig ) ) { - // Get stable key from support-specific map, common properties map, or keep original. - const stableKey = - EXPERIMENTAL_SUPPORT_PROPERTIES[ stableSupportKey ]?.[ key ] ?? - COMMON_EXPERIMENTAL_PROPERTIES[ key ] ?? - key; - - stableConfig[ stableKey ] = value; - - /* - * The `__experimentalSkipSerialization` key needs to be kept until - * WP 6.8 becomes the minimum supported version. This is due to the - * core `wp_should_skip_block_supports_serialization` function only - * checking for `__experimentalSkipSerialization` in earlier versions. - */ - if ( - key === '__experimentalSkipSerialization' || - key === 'skipSerialization' - ) { - stableConfig.__experimentalSkipSerialization = value; - } - } - return stableConfig; -} - -/** - * Stabilizes experimental block supports by converting experimental keys and properties - * to their stable equivalents. - * - * @param {Object|undefined} rawSupports The block supports configuration to stabilize. - * @return {Object|undefined} The stabilized block supports configuration. - */ -function stabilizeSupports( rawSupports ) { - if ( ! rawSupports ) { - return rawSupports; - } - - /* - * Create a new object to avoid mutating the original. This ensures that - * custom block plugins that rely on immutable supports are not affected. - * See: https://github.com/WordPress/gutenberg/pull/66849#issuecomment-2463614281 - */ - const newSupports = {}; - const done = {}; - - for ( const [ support, config ] of Object.entries( rawSupports ) ) { - /* - * If this support config has already been stabilized, skip it. - * A stable support key occurring after an experimental key, gets - * stabilized then so that the two configs can be merged effectively. - */ - if ( done[ support ] ) { - continue; - } - - const stableSupportKey = - EXPERIMENTAL_SUPPORTS_MAP[ support ] ?? support; - - /* - * Use the support's config as is when it's not in need of stabilization. - * A support does not need stabilization if: - * - The support key doesn't need stabilization AND - * - Either: - * - The config isn't an object, so can't have experimental properties OR - * - The config is an object but has no experimental properties to stabilize. - */ - if ( - support === stableSupportKey && - ( ! isPlainObject( config ) || - ( ! EXPERIMENTAL_SUPPORT_PROPERTIES[ stableSupportKey ] && - Object.keys( config ).every( - ( key ) => ! COMMON_EXPERIMENTAL_PROPERTIES[ key ] - ) ) ) - ) { - newSupports[ support ] = config; - continue; - } - - // Stabilize the config value. - const stableConfig = isPlainObject( config ) - ? stabilizeSupportConfig( config, stableSupportKey ) - : config; - - /* - * If a plugin overrides the support config with the `blocks.registerBlockType` - * filter, both experimental and stable configs may be present. In that case, - * use the order keys are defined in to determine the final value. - * - If config is an array, merge the arrays in their order of definition. - * - If config is not an array, use the value defined last. - * - * The reason for preferring the last defined key is that after filters - * are applied, the last inserted key is likely the most up-to-date value. - * We cannot determine with certainty which value was "last modified" so - * the insertion order is the best guess. The extreme edge case of multiple - * filters tweaking the same support property will become less over time as - * extenders migrate existing blocks and plugins to stable keys. - */ - if ( - support !== stableSupportKey && - Object.hasOwn( rawSupports, stableSupportKey ) - ) { - const keyPositions = Object.keys( rawSupports ).reduce( - ( acc, key, index ) => { - acc[ key ] = index; - return acc; - }, - {} - ); - const experimentalFirst = - ( keyPositions[ support ] ?? Number.MAX_VALUE ) < - ( keyPositions[ stableSupportKey ] ?? Number.MAX_VALUE ); - - if ( isPlainObject( rawSupports[ stableSupportKey ] ) ) { - /* - * To merge the alternative support config effectively, it also needs to be - * stabilized before merging to keep stabilized and experimental flags in sync. - */ - rawSupports[ stableSupportKey ] = stabilizeSupportConfig( - rawSupports[ stableSupportKey ], - stableSupportKey - ); - newSupports[ stableSupportKey ] = experimentalFirst - ? { ...stableConfig, ...rawSupports[ stableSupportKey ] } - : { ...rawSupports[ stableSupportKey ], ...stableConfig }; - // Prevents reprocessing this support as it was merged above. - done[ stableSupportKey ] = true; - } else { - newSupports[ stableSupportKey ] = experimentalFirst - ? rawSupports[ stableSupportKey ] - : stableConfig; - } - } else { - newSupports[ stableSupportKey ] = stableConfig; - } - } - - return newSupports; -} - /** * Takes the unprocessed block type settings, merges them with block type metadata * and applies all the existing filters for the registered block type. @@ -257,9 +102,6 @@ export const processBlockType = ), }; - // Stabilize any experimental supports before applying filters. - blockType.supports = stabilizeSupports( blockType.supports ); - const settings = applyFilters( 'blocks.registerBlockType', blockType, @@ -267,10 +109,6 @@ export const processBlockType = null ); - // Re-stabilize any experimental supports after applying filters. - // This ensures that any supports updated by filters are also stabilized. - blockType.supports = stabilizeSupports( blockType.supports ); - if ( settings.description && typeof settings.description !== 'string' @@ -281,40 +119,29 @@ export const processBlockType = } if ( settings.deprecated ) { - settings.deprecated = settings.deprecated.map( ( deprecation ) => { - // Stabilize any experimental supports before applying filters. - let filteredDeprecation = { - ...deprecation, - supports: stabilizeSupports( deprecation.supports ), - }; - - filteredDeprecation = // Only keep valid deprecation keys. - applyFilters( - 'blocks.registerBlockType', - // Merge deprecation keys with pre-filter settings - // so that filters that depend on specific keys being - // present don't fail. - { - // Omit deprecation keys here so that deprecations - // can opt out of specific keys like "supports". - ...omit( blockType, DEPRECATED_ENTRY_KEYS ), - ...filteredDeprecation, - }, - blockType.name, - filteredDeprecation - ); - // Re-stabilize any experimental supports after applying filters. - // This ensures that any supports updated by filters are also stabilized. - filteredDeprecation.supports = stabilizeSupports( - filteredDeprecation.supports - ); - - return Object.fromEntries( - Object.entries( filteredDeprecation ).filter( ( [ key ] ) => + settings.deprecated = settings.deprecated.map( ( deprecation ) => + Object.fromEntries( + Object.entries( + // Only keep valid deprecation keys. + applyFilters( + 'blocks.registerBlockType', + // Merge deprecation keys with pre-filter settings + // so that filters that depend on specific keys being + // present don't fail. + { + // Omit deprecation keys here so that deprecations + // can opt out of specific keys like "supports". + ...omit( blockType, DEPRECATED_ENTRY_KEYS ), + ...deprecation, + }, + blockType.name, + deprecation + ) + ).filter( ( [ key ] ) => DEPRECATED_ENTRY_KEYS.includes( key ) ) - ); - } ); + ) + ); } if ( ! isPlainObject( settings ) ) { diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index c4589ce8232f66..e20938fd84e2b4 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -102,7 +102,7 @@ export const getBlockTypes = createSelector( * }; * ``` * - * @return {Object?} Block Type. + * @return {?Object} Block Type. */ export function getBlockType( state, name ) { return state.blockTypes[ name ]; diff --git a/packages/blocks/src/store/test/private-selectors.js b/packages/blocks/src/store/test/private-selectors.js index 2c173b96b0bcb1..ada2bd7c8cbcfe 100644 --- a/packages/blocks/src/store/test/private-selectors.js +++ b/packages/blocks/src/store/test/private-selectors.js @@ -127,12 +127,12 @@ describe( 'private selectors', () => { name: 'core/example-block', supports: { typography: { - fontFamily: true, - fontStyle: true, - fontWeight: true, - textDecoration: true, - textTransform: true, - letterSpacing: true, + __experimentalFontFamily: true, + __experimentalFontStyle: true, + __experimentalFontWeight: true, + __experimentalTextDecoration: true, + __experimentalTextTransform: true, + __experimentalLetterSpacing: true, fontSize: true, lineHeight: true, }, diff --git a/packages/blocks/src/store/test/process-block-type.js b/packages/blocks/src/store/test/process-block-type.js deleted file mode 100644 index 82b2c1ad3080d7..00000000000000 --- a/packages/blocks/src/store/test/process-block-type.js +++ /dev/null @@ -1,490 +0,0 @@ -/** - * WordPress dependencies - */ -import { addFilter, removeFilter } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import { processBlockType } from '../process-block-type'; - -describe( 'processBlockType', () => { - const baseBlockSettings = { - apiVersion: 3, - attributes: {}, - edit: () => null, - name: 'test/block', - save: () => null, - title: 'Test Block', - }; - - const select = { - getBootstrappedBlockType: () => null, - }; - - afterEach( () => { - removeFilter( 'blocks.registerBlockType', 'test/filterSupports' ); - } ); - - it( 'should stabilize experimental block supports', () => { - const blockSettings = { - ...baseBlockSettings, - supports: { - typography: { - fontSize: true, - lineHeight: true, - __experimentalFontFamily: true, - __experimentalFontStyle: true, - __experimentalFontWeight: true, - __experimentalLetterSpacing: true, - __experimentalTextTransform: true, - __experimentalTextDecoration: true, - __experimentalWritingMode: true, - __experimentalDefaultControls: { - fontSize: true, - fontAppearance: true, - textTransform: true, - }, - }, - __experimentalBorder: { - color: true, - radius: true, - style: true, - width: true, - __experimentalDefaultControls: { - color: true, - radius: true, - style: true, - width: true, - }, - }, - }, - }; - - const processedBlockType = processBlockType( - 'test/block', - blockSettings - )( { select } ); - - expect( processedBlockType.supports ).toMatchObject( { - typography: { - fontSize: true, - lineHeight: true, - fontFamily: true, - fontStyle: true, - fontWeight: true, - letterSpacing: true, - textTransform: true, - textDecoration: true, - __experimentalWritingMode: true, - defaultControls: { - fontSize: true, - fontAppearance: true, - textTransform: true, - }, - }, - border: { - color: true, - radius: true, - style: true, - width: true, - defaultControls: { - color: true, - radius: true, - style: true, - width: true, - }, - }, - } ); - } ); - - it( 'should reapply transformations after supports are filtered', () => { - const blockSettings = { - ...baseBlockSettings, - supports: { - typography: { - fontSize: true, - lineHeight: true, - __experimentalFontFamily: true, - __experimentalFontStyle: true, - __experimentalFontWeight: true, - __experimentalLetterSpacing: true, - __experimentalTextTransform: true, - __experimentalTextDecoration: true, - __experimentalWritingMode: true, - __experimentalDefaultControls: { - fontSize: true, - fontAppearance: true, - textTransform: true, - }, - }, - __experimentalBorder: { - color: true, - radius: true, - style: true, - width: true, - __experimentalDefaultControls: { - color: true, - radius: true, - style: true, - width: true, - }, - }, - }, - }; - - addFilter( - 'blocks.registerBlockType', - 'test/filterSupports', - ( settings, name ) => { - if ( name === 'test/block' && settings.supports.typography ) { - settings.supports.typography.__experimentalFontFamily = false; - settings.supports.typography.__experimentalFontStyle = false; - settings.supports.typography.__experimentalFontWeight = false; - if ( ! settings.supports.__experimentalBorder ) { - settings.supports.__experimentalBorder = {}; - } - settings.supports.__experimentalBorder.radius = false; - } - return settings; - } - ); - - const processedBlockType = processBlockType( - 'test/block', - blockSettings - )( { select } ); - - expect( processedBlockType.supports ).toMatchObject( { - typography: { - fontSize: true, - lineHeight: true, - fontFamily: false, - fontStyle: false, - fontWeight: false, - letterSpacing: true, - textTransform: true, - textDecoration: true, - __experimentalWritingMode: true, - defaultControls: { - fontSize: true, - fontAppearance: true, - textTransform: true, - }, - }, - border: { - color: true, - radius: false, - style: true, - width: true, - defaultControls: { - color: true, - radius: true, - style: true, - width: true, - }, - }, - } ); - } ); - - describe( 'block deprecations', () => { - const deprecatedBlockSettings = { - ...baseBlockSettings, - supports: { - typography: { - fontSize: true, - lineHeight: true, - fontFamily: true, - fontStyle: true, - fontWeight: true, - letterSpacing: true, - textTransform: true, - textDecoration: true, - __experimentalWritingMode: true, - __experimentalDefaultControls: { - fontSize: true, - fontAppearance: true, - textTransform: true, - }, - }, - border: { - color: true, - radius: true, - style: true, - width: true, - __experimentalDefaultControls: { - color: true, - radius: true, - style: true, - width: true, - }, - }, - }, - deprecated: [ - { - supports: { - typography: { - __experimentalFontFamily: true, - __experimentalFontStyle: true, - __experimentalFontWeight: true, - __experimentalLetterSpacing: true, - __experimentalTextTransform: true, - __experimentalTextDecoration: true, - __experimentalWritingMode: true, - }, - __experimentalBorder: { - color: true, - radius: true, - style: true, - width: true, - __experimentalDefaultControls: { - color: true, - radius: true, - style: true, - width: true, - }, - }, - }, - }, - ], - }; - - beforeEach( () => { - // Freeze the deprecated block object and its supports so that the original is not mutated. - Object.freeze( deprecatedBlockSettings.deprecated[ 0 ] ); - Object.freeze( deprecatedBlockSettings.deprecated[ 0 ].supports ); - } ); - - it( 'should stabilize experimental supports', () => { - const processedBlockType = processBlockType( - 'test/block', - deprecatedBlockSettings - )( { select } ); - - expect( processedBlockType.deprecated[ 0 ].supports ).toMatchObject( - { - typography: { - fontFamily: true, - fontStyle: true, - fontWeight: true, - letterSpacing: true, - textTransform: true, - textDecoration: true, - __experimentalWritingMode: true, - }, - border: { - color: true, - radius: true, - style: true, - width: true, - defaultControls: { - color: true, - radius: true, - style: true, - width: true, - }, - }, - } - ); - } ); - - it( 'should reapply transformations after supports are filtered', () => { - addFilter( - 'blocks.registerBlockType', - 'test/filterSupports', - ( settings, name ) => { - if ( - name === 'test/block' && - settings.supports.typography - ) { - settings.supports.typography.__experimentalFontFamily = false; - settings.supports.typography.__experimentalFontStyle = false; - settings.supports.typography.__experimentalFontWeight = false; - settings.supports.__experimentalBorder = { - radius: false, - }; - } - return settings; - } - ); - - const processedBlockType = processBlockType( - 'test/block', - deprecatedBlockSettings - )( { select } ); - - expect( processedBlockType.deprecated[ 0 ].supports ).toMatchObject( - { - typography: { - fontFamily: false, - fontStyle: false, - fontWeight: false, - letterSpacing: true, - textTransform: true, - textDecoration: true, - __experimentalWritingMode: true, - }, - border: { - color: true, - radius: false, - style: true, - width: true, - defaultControls: { - color: true, - radius: true, - style: true, - width: true, - }, - }, - } - ); - } ); - } ); - - it( 'should stabilize common experimental properties across all supports', () => { - const blockSettings = { - ...baseBlockSettings, - supports: { - typography: { - fontSize: true, - __experimentalDefaultControls: { - fontSize: true, - }, - __experimentalSkipSerialization: true, - }, - spacing: { - padding: true, - __experimentalDefaultControls: { - padding: true, - }, - __experimentalSkipSerialization: true, - }, - }, - }; - - const processedBlockType = processBlockType( - 'test/block', - blockSettings - )( { select } ); - - expect( processedBlockType.supports ).toMatchObject( { - typography: { - fontSize: true, - defaultControls: { - fontSize: true, - }, - skipSerialization: true, - __experimentalSkipSerialization: true, - }, - spacing: { - padding: true, - defaultControls: { - padding: true, - }, - skipSerialization: true, - __experimentalSkipSerialization: true, - }, - } ); - } ); - - it( 'should merge experimental and stable keys in order of definition', () => { - const blockSettings = { - ...baseBlockSettings, - supports: { - __experimentalBorder: { - color: true, - radius: false, - }, - border: { - color: false, - style: true, - }, - }, - }; - - const processedBlockType = processBlockType( - 'test/block', - blockSettings - )( { select } ); - - expect( processedBlockType.supports ).toMatchObject( { - border: { - color: false, - radius: false, - style: true, - }, - } ); - - const reversedSettings = { - ...baseBlockSettings, - supports: { - border: { - color: false, - style: true, - }, - __experimentalBorder: { - color: true, - radius: false, - }, - }, - }; - - const reversedProcessedType = processBlockType( - 'test/block', - reversedSettings - )( { select } ); - - expect( reversedProcessedType.supports ).toMatchObject( { - border: { - color: true, - radius: false, - style: true, - }, - } ); - } ); - - it( 'should handle non-object config values', () => { - const blockSettings = { - ...baseBlockSettings, - supports: { - __experimentalBorder: true, - border: false, - }, - }; - - const processedBlockType = processBlockType( - 'test/block', - blockSettings - )( { select } ); - - expect( processedBlockType.supports ).toMatchObject( { - border: false, - } ); - } ); - - it( 'should not modify supports that do not need stabilization', () => { - const blockSettings = { - ...baseBlockSettings, - supports: { - align: true, - spacing: { - padding: true, - margin: true, - }, - }, - }; - - const processedBlockType = processBlockType( - 'test/block', - blockSettings - )( { select } ); - - expect( processedBlockType.supports ).toMatchObject( { - align: true, - spacing: { - padding: true, - margin: true, - }, - } ); - } ); -} ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index c4c47f43024007..8c710eb93a9ee5 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,40 @@ ## Unreleased +### Enhancements + +- `BoxControl`: Add presets support ([#67688](https://github.com/WordPress/gutenberg/pull/67688)). +- `Navigation`: Upsize back buttons ([#68157](https://github.com/WordPress/gutenberg/pull/68157)). +- `Heading`: Fix text contrast for dark mode ([#68349](https://github.com/WordPress/gutenberg/pull/68349)). +- `Text`: Fix text contrast for dark mode ([#68349](https://github.com/WordPress/gutenberg/pull/68349)). + +### Deprecations + +- `TreeSelect`: Deprecate 36px default size ([#67855](https://github.com/WordPress/gutenberg/pull/67855)). +- `SelectControl`: Deprecate 36px default size ([#66898](https://github.com/WordPress/gutenberg/pull/66898)). +- `InputControl`: Deprecate 36px default size ([#66897](https://github.com/WordPress/gutenberg/pull/66897)). +- `RadioGroup`: Log deprecation warning ([#68067](https://github.com/WordPress/gutenberg/pull/68067)). +- Soft deprecate `ButtonGroup` component. Use `ToggleGroupControl` instead ([#65429](https://github.com/WordPress/gutenberg/pull/65429)). +- `Navigation`: Log deprecation warning for removal in WP 7.1. Use `Navigator` instead ([#68158](https://github.com/WordPress/gutenberg/pull/68158)). + +### Bug Fixes + +- `BoxControl`: Better respect for the `min` prop in the Range Slider ([#67819](https://github.com/WordPress/gutenberg/pull/67819)). +- `FontSizePicker`: Add `display:contents` rule to fix overflowing text in the custom size select. ([#68280](https://github.com/WordPress/gutenberg/pull/68280)). +- `BoxControl`: Fix aria-valuetext value ([#68362](https://github.com/WordPress/gutenberg/pull/68362)). + +### Experimental + +- Add new `Badge` component ([#66555](https://github.com/WordPress/gutenberg/pull/66555)). +- `Menu`: refactor to more granular sub-components ([#67422](https://github.com/WordPress/gutenberg/pull/67422)). +- `Badge`: Support text truncation ([#68107](https://github.com/WordPress/gutenberg/pull/68107)). + +### Internal + +- `SlotFill`: rewrite the non-portal version to use `observableMap` ([#67400](https://github.com/WordPress/gutenberg/pull/67400)). +- `DatePicker`: Prepare day buttons for 40px default size ([#68156](https://github.com/WordPress/gutenberg/pull/68156)). +- `SlotFill`: register slots in a layout effect ([#68176](https://github.com/WordPress/gutenberg/pull/68176)). + ## 29.0.0 (2024-12-11) ### Breaking Changes @@ -15,12 +49,13 @@ - `Menu`: Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). - `Navigation` (deprecated): Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). - `ToggleGroupControl`: Replace hardcoded white color with theme-ready variable ([#67649](https://github.com/WordPress/gutenberg/pull/67649)). -- `RangeControl`: Update the design of the range control marks ([#67611](https://github.com/WordPress/gutenberg/pull/67611)) +- `RangeControl`: Update the design of the range control marks ([#67611](https://github.com/WordPress/gutenberg/pull/67611)). - `BorderBoxControl`: Reduce gap value when unlinked ([#67049](https://github.com/WordPress/gutenberg/pull/67049)). - `DropdownMenu`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). - `MenuItem`: Increase height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). - `MenuItemsChoice`: Increase option height to 40px ([#67435](https://github.com/WordPress/gutenberg/pull/67435)). - `Navigation`: Fix active item hover color ([#67732](https://github.com/WordPress/gutenberg/pull/67732)). +- `Button`: Adjust `secondary` variant hover style. ([#67325](https://github.com/WordPress/gutenberg/pull/67325)). ### Deprecations @@ -48,6 +83,7 @@ - `ResizableBox`: Make drag handles focusable ([#67305](https://github.com/WordPress/gutenberg/pull/67305)). - `CustomSelectControl`: Update correctly when `showSelectedHint` is enabled ([#67733](https://github.com/WordPress/gutenberg/pull/67733)). +- `CustomSelectControl`: Use `useStoreState` to get `currentValue` and avoid stale values ([#67815](https://github.com/WordPress/gutenberg/pull/67815)). ## 28.13.0 (2024-11-27) diff --git a/packages/components/src/alignment-matrix-control/README.md b/packages/components/src/alignment-matrix-control/README.md index af97e3ae0607cd..8ba9f6378c1852 100644 --- a/packages/components/src/alignment-matrix-control/README.md +++ b/packages/components/src/alignment-matrix-control/README.md @@ -21,47 +21,48 @@ const Example = () => { ); }; ``` + ## Props ### `defaultValue` -If provided, sets the default alignment value. - - Type: `"center" | "top left" | "top center" | "top right" | "center left" | "center center" | "center right" | "bottom left" | "bottom center" | "bottom right"` - Required: No - Default: `'center center'` -### `label` +If provided, sets the default alignment value. -Accessible label. If provided, sets the `aria-label` attribute of the -underlying `grid` widget. +### `label` - Type: `string` - Required: No - Default: `'Alignment Matrix Control'` -### `onChange` +Accessible label. If provided, sets the `aria-label` attribute of the +underlying `grid` widget. -A function that receives the updated alignment value. +### `onChange` - Type: `(newValue: AlignmentMatrixControlValue) => void` - Required: No -### `value` +A function that receives the updated alignment value. -The current alignment value. +### `value` - Type: `"center" | "top left" | "top center" | "top right" | "center left" | "center center" | "center right" | "bottom left" | "bottom center" | "bottom right"` - Required: No -### `width` +The current alignment value. -If provided, sets the width of the control. +### `width` - Type: `number` - Required: No - Default: `92` +If provided, sets the width of the control. + ## Subcomponents ### AlignmentMatrixControl.Icon @@ -70,16 +71,16 @@ If provided, sets the width of the control. ##### `disablePointerEvents` -If `true`, disables pointer events on the icon. - - Type: `boolean` - Required: No - Default: `true` -##### `value` +If `true`, disables pointer events on the icon. -The current alignment value. +##### `value` - Type: `"center" | "top left" | "top center" | "top right" | "center left" | "center center" | "center right" | "bottom left" | "bottom center" | "bottom right"` - Required: No - Default: `center` + +The current alignment value. diff --git a/packages/components/src/angle-picker-control/README.md b/packages/components/src/angle-picker-control/README.md index d9389c6564338f..9908282fd9ef9a 100644 --- a/packages/components/src/angle-picker-control/README.md +++ b/packages/components/src/angle-picker-control/README.md @@ -23,34 +23,35 @@ function Example() { ); } ``` + ## Props ### `as` -The HTML element or React component to render the component as. - - Type: `"symbol" | "object" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | "b" | ...` - Required: No -### `label` +The HTML element or React component to render the component as. -Label to use for the angle picker. +### `label` - Type: `string` - Required: No - Default: `__( 'Angle' )` -### `onChange` +Label to use for the angle picker. -A function that receives the new value of the input. +### `onChange` - Type: `(value: number) => void` - Required: Yes -### `value` +A function that receives the new value of the input. -The current value of the input. The value represents an angle in degrees -and should be a value between 0 and 360. +### `value` - Type: `string | number` - Required: Yes + +The current value of the input. The value represents an angle in degrees +and should be a value between 0 and 360. diff --git a/packages/components/src/badge/README.md b/packages/components/src/badge/README.md new file mode 100644 index 00000000000000..2100939684a856 --- /dev/null +++ b/packages/components/src/badge/README.md @@ -0,0 +1,24 @@ +# Badge + +<!-- This file is generated automatically and cannot be edited directly. Make edits via TypeScript types and TSDocs. --> + +🔒 This component is locked as a [private API](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-private-apis/). We do not yet recommend using this outside of the Gutenberg project. + +<p class="callout callout-info">See the <a href="https://wordpress.github.io/gutenberg/?path=/docs/components-badge--docs">WordPress Storybook</a> for more detailed, interactive documentation.</p> + +## Props + +### `children` + + - Type: `string` + - Required: Yes + +Text to display inside the badge. + +### `intent` + + - Type: `"default" | "info" | "success" | "warning" | "error"` + - Required: No + - Default: `default` + +Badge variant. diff --git a/packages/components/src/badge/docs-manifest.json b/packages/components/src/badge/docs-manifest.json new file mode 100644 index 00000000000000..3b70c0ef228432 --- /dev/null +++ b/packages/components/src/badge/docs-manifest.json @@ -0,0 +1,5 @@ +{ + "$schema": "../../schemas/docs-manifest.json", + "displayName": "Badge", + "filePath": "./index.tsx" +} diff --git a/packages/components/src/badge/index.tsx b/packages/components/src/badge/index.tsx new file mode 100644 index 00000000000000..ee08003c3911dc --- /dev/null +++ b/packages/components/src/badge/index.tsx @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { info, caution, error, published } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import type { BadgeProps } from './types'; +import type { WordPressComponentProps } from '../context'; +import Icon from '../icon'; + +function Badge( { + className, + intent = 'default', + children, + ...props +}: WordPressComponentProps< BadgeProps, 'span', false > ) { + /** + * Returns an icon based on the badge context. + * + * @return The corresponding icon for the provided context. + */ + function contextBasedIcon() { + switch ( intent ) { + case 'info': + return info; + case 'success': + return published; + case 'warning': + return caution; + case 'error': + return error; + default: + return null; + } + } + + return ( + <span + className={ clsx( + 'components-badge', + `is-${ intent }`, + intent !== 'default' && 'has-icon', + className + ) } + { ...props } + > + { intent !== 'default' && ( + <Icon + icon={ contextBasedIcon() } + size={ 16 } + fill="currentColor" + className="components-badge__icon" + /> + ) } + <span className="components-badge__content">{ children }</span> + </span> + ); +} + +export default Badge; diff --git a/packages/components/src/badge/stories/index.story.tsx b/packages/components/src/badge/stories/index.story.tsx new file mode 100644 index 00000000000000..bbe0bef2a79472 --- /dev/null +++ b/packages/components/src/badge/stories/index.story.tsx @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import type { Meta, StoryObj } from '@storybook/react'; + +/** + * Internal dependencies + */ +import Badge from '..'; + +const meta: Meta< typeof Badge > = { + component: Badge, + title: 'Components/Containers/Badge', + id: 'components-badge', + tags: [ 'status-private' ], +}; + +export default meta; + +type Story = StoryObj< typeof meta >; + +export const Default: Story = { + args: { + children: 'Code is Poetry', + }, +}; + +export const Info: Story = { + args: { + ...Default.args, + intent: 'info', + }, +}; + +export const Success: Story = { + args: { + ...Default.args, + intent: 'success', + }, +}; + +export const Warning: Story = { + args: { + ...Default.args, + intent: 'warning', + }, +}; + +export const Error: Story = { + args: { + ...Default.args, + intent: 'error', + }, +}; diff --git a/packages/components/src/badge/styles.scss b/packages/components/src/badge/styles.scss new file mode 100644 index 00000000000000..d3f82482cf7743 --- /dev/null +++ b/packages/components/src/badge/styles.scss @@ -0,0 +1,49 @@ +$badge-colors: ( + "info": #3858e9, + "warning": $alert-yellow, + "error": $alert-red, + "success": $alert-green, +); + +.components-badge { + @include reset; + + background-color: color-mix(in srgb, $white 90%, var(--base-color)); + color: color-mix(in srgb, $black 50%, var(--base-color)); + padding: 0 $grid-unit-10; + min-height: $grid-unit-30; + max-width: 100%; + border-radius: $radius-small; + font-size: $font-size-small; + font-weight: 400; + line-height: $font-line-height-small; + display: inline-flex; + align-items: center; + gap: 2px; + + &:where(.is-default) { + background-color: $gray-100; + color: $gray-800; + } + + &.has-icon { + padding-inline-start: $grid-unit-05; + } + + // Generate color variants + @each $type, $color in $badge-colors { + &.is-#{$type} { + --base-color: #{$color}; + } + } +} + +.components-badge__icon { + flex-shrink: 0; +} + +.components-badge__content { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/packages/components/src/badge/test/index.tsx b/packages/components/src/badge/test/index.tsx new file mode 100644 index 00000000000000..114a8f426c7afd --- /dev/null +++ b/packages/components/src/badge/test/index.tsx @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import _Badge from '..'; + +const testid = 'my-badge'; +const Badge = ( props: React.ComponentProps< typeof _Badge > ) => ( + <_Badge data-testid={ testid } { ...props } /> +); + +describe( 'Badge', () => { + it( 'should render correctly with default props', () => { + render( <Badge>Code is Poetry</Badge> ); + const badge = screen.getByTestId( testid ); + expect( badge ).toBeInTheDocument(); + expect( badge.tagName ).toBe( 'SPAN' ); + expect( badge ).toHaveClass( 'components-badge' ); + } ); + + it( 'should render as per its intent and contain an icon', () => { + render( <Badge intent="error">Code is Poetry</Badge> ); + const badge = screen.getByTestId( testid ); + expect( badge ).toHaveClass( 'components-badge', 'is-error' ); + expect( badge ).toHaveClass( 'has-icon' ); + } ); + + it( 'should combine custom className with default class', () => { + render( <Badge className="custom-class">Code is Poetry</Badge> ); + const badge = screen.getByTestId( testid ); + expect( badge ).toHaveClass( 'components-badge' ); + expect( badge ).toHaveClass( 'custom-class' ); + } ); + + it( 'should pass through additional props', () => { + render( <Badge data-testid="custom-badge">Code is Poetry</Badge> ); + const badge = screen.getByTestId( 'custom-badge' ); + expect( badge ).toHaveTextContent( 'Code is Poetry' ); + expect( badge ).toHaveClass( 'components-badge' ); + } ); +} ); diff --git a/packages/components/src/badge/types.ts b/packages/components/src/badge/types.ts new file mode 100644 index 00000000000000..91cd7c39b549bb --- /dev/null +++ b/packages/components/src/badge/types.ts @@ -0,0 +1,12 @@ +export type BadgeProps = { + /** + * Badge variant. + * + * @default 'default' + */ + intent?: 'default' | 'info' | 'success' | 'warning' | 'error'; + /** + * Text to display inside the badge. + */ + children: string; +}; diff --git a/packages/components/src/base-control/README.md b/packages/components/src/base-control/README.md index 839464b41260b5..2a82c19845e47b 100644 --- a/packages/components/src/base-control/README.md +++ b/packages/components/src/base-control/README.md @@ -25,71 +25,71 @@ const MyCustomTextareaControl = ({ children, ...baseProps }) => ( ); ); ``` + ## Props ### `__nextHasNoMarginBottom` -Start opting into the new margin-free styles that will become the default in a future version. - - Type: `boolean` - Required: No - Default: `false` -### `as` +Start opting into the new margin-free styles that will become the default in a future version. -The HTML element or React component to render the component as. +### `as` - Type: `"symbol" | "object" | "label" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | "b" | "base" | "bdi" | "bdo" | "big" | "blockquote" | "body" | "br" | "button" | ... 516 more ... | ("view" & FunctionComponent<...>)` - Required: No -### `className` +The HTML element or React component to render the component as. +### `className` - Type: `string` - Required: No ### `children` -The content to be displayed within the `BaseControl`. - - Type: `ReactNode` - Required: Yes +The content to be displayed within the `BaseControl`. + ### `help` + - Type: `ReactNode` + - Required: No + Additional description for the control. Only use for meaningful description or instructions for the control. An element containing the description will be programmatically associated to the BaseControl by the means of an `aria-describedby` attribute. - - Type: `ReactNode` - - Required: No - ### `hideLabelFromVision` -If true, the label will only be visible to screen readers. - - Type: `boolean` - Required: No - Default: `false` +If true, the label will only be visible to screen readers. + ### `id` + - Type: `string` + - Required: No + The HTML `id` of the control element (passed in as a child to `BaseControl`) to which labels and help text are being generated. This is necessary to accessibly associate the label with that element. The recommended way is to use the `useBaseControlProps` hook, which takes care of generating a unique `id` for you. Otherwise, if you choose to pass an explicit `id` to this prop, you are responsible for ensuring the uniqueness of the `id`. - - Type: `string` - - Required: No - ### `label` -If this property is added, a label will be generated using label property as the content. - - Type: `ReactNode` - Required: No +If this property is added, a label will be generated using label property as the content. + ## Subcomponents ### BaseControl.VisualLabel @@ -113,18 +113,19 @@ const MyBaseControl = () => ( </BaseControl> ); ``` + #### Props ##### `as` -The HTML element or React component to render the component as. - - Type: `"symbol" | "object" | "label" | "a" | "abbr" | "address" | "area" | "article" | "aside" | "audio" | ...` - Required: No -##### `children` +The HTML element or React component to render the component as. -The content to be displayed within the `BaseControl.VisualLabel`. +##### `children` - Type: `ReactNode` - Required: Yes + +The content to be displayed within the `BaseControl.VisualLabel`. diff --git a/packages/components/src/box-control/README.md b/packages/components/src/box-control/README.md index 8bcb5a5dad8fc2..4c0f100065092e 100644 --- a/packages/components/src/box-control/README.md +++ b/packages/components/src/box-control/README.md @@ -28,34 +28,33 @@ function Example() { ); }; ``` + ## Props ### `__next40pxDefaultSize` -Start opting into the larger default height that will become the default size in a future version. - - Type: `boolean` - Required: No - Default: `false` -### `allowReset` +Start opting into the larger default height that will become the default size in a future version. -If this property is true, a button to reset the box control is rendered. +### `allowReset` - Type: `boolean` - Required: No - Default: `true` -### `id` +If this property is true, a button to reset the box control is rendered. -The id to use as a base for the unique HTML id attribute of the control. +### `id` - Type: `string` - Required: No -### `inputProps` +The id to use as a base for the unique HTML id attribute of the control. -Props for the internal `UnitControl` components. +### `inputProps` - Type: `UnitControlPassthroughProps` - Required: No @@ -63,25 +62,41 @@ Props for the internal `UnitControl` components. min: 0, }` -### `label` +Props for the internal `UnitControl` components. -Heading label for the control. +### `label` - Type: `string` - Required: No - Default: `__( 'Box Control' )` -### `onChange` +Heading label for the control. -A callback function when an input value changes. +### `onChange` - Type: `(next: BoxControlValue) => void` - Required: No - Default: `() => {}` -### `resetValues` +A callback function when an input value changes. -The `top`, `right`, `bottom`, and `left` box dimension values to use when the control is reset. +### `presets` + + - Type: `Preset[]` + - Required: No + +Available presets to pick from. + +### `presetKey` + + - Type: `string` + - Required: No + +The key of the preset to apply. +If you provide a list of presets, you must provide a preset key to use. +The format of preset selected values is going to be `var:preset|${ presetKey }|${ presetSlug }` + +### `resetValues` - Type: `BoxControlValue` - Required: No @@ -92,35 +107,37 @@ The `top`, `right`, `bottom`, and `left` box dimension values to use when the co left: undefined, }` +The `top`, `right`, `bottom`, and `left` box dimension values to use when the control is reset. + ### `sides` + - Type: `readonly (keyof BoxControlValue | "horizontal" | "vertical")[]` + - Required: No + Collection of sides to allow control of. If omitted or empty, all sides will be available. Allowed values are "top", "right", "bottom", "left", "vertical", and "horizontal". - - Type: `readonly (keyof BoxControlValue | "horizontal" | "vertical")[]` - - Required: No - ### `splitOnAxis` -If this property is true, when the box control is unlinked, vertical and horizontal controls -can be used instead of updating individual sides. - - Type: `boolean` - Required: No - Default: `false` -### `units` +If this property is true, when the box control is unlinked, vertical and horizontal controls +can be used instead of updating individual sides. -Available units to select from. +### `units` - Type: `WPUnitControlUnit[]` - Required: No - Default: `CSS_UNITS` -### `values` +Available units to select from. -The current values of the control, expressed as an object of `top`, `right`, `bottom`, and `left` values. +### `values` - Type: `BoxControlValue` - Required: No + +The current values of the control, expressed as an object of `top`, `right`, `bottom`, and `left` values. diff --git a/packages/components/src/box-control/index.tsx b/packages/components/src/box-control/index.tsx index 279dfa55eafe38..d4d4b03f893036 100644 --- a/packages/components/src/box-control/index.tsx +++ b/packages/components/src/box-control/index.tsx @@ -83,6 +83,8 @@ function BoxControl( { splitOnAxis = false, allowReset = true, resetValues = DEFAULT_VALUES, + presets, + presetKey, onMouseOver, onMouseOut, }: BoxControlProps ) { @@ -153,6 +155,8 @@ function BoxControl( { sides, values: inputValues, __next40pxDefaultSize, + presets, + presetKey, }; maybeWarnDeprecated36pxSize( { diff --git a/packages/components/src/box-control/input-control.tsx b/packages/components/src/box-control/input-control.tsx index 9086cebedc2749..27dff1991d8572 100644 --- a/packages/components/src/box-control/input-control.tsx +++ b/packages/components/src/box-control/input-control.tsx @@ -3,6 +3,8 @@ */ import { useInstanceId } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { settings } from '@wordpress/icons'; /** * Internal dependencies @@ -11,10 +13,13 @@ import Tooltip from '../tooltip'; import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; import { CUSTOM_VALUE_SETTINGS, - getAllowedSides, getMergedValue, - isValueMixed, + getAllowedSides, + getPresetIndexFromValue, + getPresetValueFromIndex, + isValuePreset, isValuesDefined, + isValueMixed, LABELS, } from './utils'; import { @@ -24,6 +29,7 @@ import { StyledUnitControl, } from './styles/box-control-styles'; import type { BoxControlInputControlProps, BoxControlValue } from './types'; +import Button from '../button'; const noop = () => {}; @@ -78,6 +84,9 @@ export default function BoxInputControl( { setSelectedUnits, sides, side, + min = 0, + presets, + presetKey, ...props }: BoxControlInputControlProps ) { const defaultValuesToModify = getSidesToModify( side, sides ); @@ -90,6 +99,15 @@ export default function BoxInputControl( { onChange( nextValues ); }; + const handleRawOnValueChange = ( next?: string ) => { + const nextValues = { ...values }; + defaultValuesToModify.forEach( ( modifiedSide ) => { + nextValues[ modifiedSide ] = next; + } ); + + handleOnChange( nextValues ); + }; + const handleOnValueChange = ( next?: string, extra?: { event: React.SyntheticEvent< Element, Event > } @@ -147,51 +165,135 @@ export default function BoxInputControl( { const usedValue = mergedValue === undefined && computedUnit ? computedUnit : mergedValue; const mixedPlaceholder = isMixed || isMixedUnit ? __( 'Mixed' ) : undefined; + const hasPresets = presets && presets.length > 0 && presetKey; + const hasPresetValue = + hasPresets && + mergedValue !== undefined && + ! isMixed && + isValuePreset( mergedValue, presetKey ); + const [ showCustomValueControl, setShowCustomValueControl ] = useState( + ! hasPresets || + ( ! hasPresetValue && ! isMixed && mergedValue !== undefined ) + ); + const presetIndex = hasPresetValue + ? getPresetIndexFromValue( mergedValue, presetKey, presets ) + : undefined; + const marks = hasPresets + ? [ { value: 0, label: '', tooltip: __( 'None' ) } ].concat( + presets.map( ( preset, index ) => ( { + value: index + 1, + label: '', + tooltip: preset.name ?? preset.slug, + } ) ) + ) + : []; return ( <InputWrapper key={ `box-control-${ side }` } expanded> <FlexedBoxControlIcon side={ side } sides={ sides } /> - <Tooltip placement="top-end" text={ LABELS[ side ] }> - <StyledUnitControl - { ...props } - __shouldNotWarnDeprecated36pxSize - __next40pxDefaultSize={ __next40pxDefaultSize } - className="component-box-control__unit-control" - id={ inputId } - isPressEnterToChange - disableUnits={ isMixed || isMixedUnit } - value={ usedValue } - onChange={ handleOnValueChange } - onUnitChange={ handleOnUnitChange } - onFocus={ handleOnFocus } + { showCustomValueControl && ( + <> + <Tooltip placement="top-end" text={ LABELS[ side ] }> + <StyledUnitControl + { ...props } + min={ min } + __shouldNotWarnDeprecated36pxSize + __next40pxDefaultSize={ __next40pxDefaultSize } + className="component-box-control__unit-control" + id={ inputId } + isPressEnterToChange + disableUnits={ isMixed || isMixedUnit } + value={ usedValue } + onChange={ handleOnValueChange } + onUnitChange={ handleOnUnitChange } + onFocus={ handleOnFocus } + label={ LABELS[ side ] } + placeholder={ mixedPlaceholder } + hideLabelFromVision + /> + </Tooltip> + + <FlexedRangeControl + __nextHasNoMarginBottom + __next40pxDefaultSize={ __next40pxDefaultSize } + __shouldNotWarnDeprecated36pxSize + aria-controls={ inputId } + label={ LABELS[ side ] } + hideLabelFromVision + onChange={ ( newValue ) => { + handleOnValueChange( + newValue !== undefined + ? [ newValue, computedUnit ].join( '' ) + : undefined + ); + } } + min={ isFinite( min ) ? min : 0 } + max={ + CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ] + ?.max ?? 10 + } + step={ + CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ] + ?.step ?? 0.1 + } + value={ parsedQuantity ?? 0 } + withInputField={ false } + /> + </> + ) } + + { hasPresets && ! showCustomValueControl && ( + <FlexedRangeControl + __next40pxDefaultSize + className="spacing-sizes-control__range-control" + value={ presetIndex !== undefined ? presetIndex + 1 : 0 } + onChange={ ( newIndex ) => { + const newValue = + newIndex === 0 || newIndex === undefined + ? undefined + : getPresetValueFromIndex( + newIndex - 1, + presetKey, + presets + ); + handleRawOnValueChange( newValue ); + } } + withInputField={ false } + aria-valuenow={ + presetIndex !== undefined ? presetIndex + 1 : 0 + } + aria-valuetext={ + marks[ presetIndex !== undefined ? presetIndex + 1 : 0 ] + .tooltip + } + renderTooltipContent={ ( index ) => + marks[ ! index ? 0 : index ].tooltip + } + min={ 0 } + max={ marks.length - 1 } + marks={ marks } label={ LABELS[ side ] } - placeholder={ mixedPlaceholder } hideLabelFromVision + __nextHasNoMarginBottom + /> + ) } + + { hasPresets && ( + <Button + label={ + showCustomValueControl + ? __( 'Use size preset' ) + : __( 'Set custom size' ) + } + icon={ settings } + onClick={ () => { + setShowCustomValueControl( ! showCustomValueControl ); + } } + isPressed={ showCustomValueControl } + size="small" + iconSize={ 24 } /> - </Tooltip> - - <FlexedRangeControl - __nextHasNoMarginBottom - __next40pxDefaultSize={ __next40pxDefaultSize } - __shouldNotWarnDeprecated36pxSize - aria-controls={ inputId } - label={ LABELS[ side ] } - hideLabelFromVision - onChange={ ( newValue ) => { - handleOnValueChange( - newValue !== undefined - ? [ newValue, computedUnit ].join( '' ) - : undefined - ); - } } - min={ 0 } - max={ CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ]?.max ?? 10 } - step={ - CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ]?.step ?? 0.1 - } - value={ parsedQuantity ?? 0 } - withInputField={ false } - /> + ) } </InputWrapper> ); } diff --git a/packages/components/src/box-control/stories/index.story.tsx b/packages/components/src/box-control/stories/index.story.tsx index 0d8b96de063168..aa16547d24ab18 100644 --- a/packages/components/src/box-control/stories/index.story.tsx +++ b/packages/components/src/box-control/stories/index.story.tsx @@ -81,3 +81,15 @@ AxialControlsWithSingleSide.args = { sides: [ 'horizontal' ], splitOnAxis: true, }; + +export const ControlWithPresets = TemplateControlled.bind( {} ); +ControlWithPresets.args = { + ...Default.args, + presets: [ + { name: 'Small', slug: 'small', value: '4px' }, + { name: 'Medium', slug: 'medium', value: '8px' }, + { name: 'Large', slug: 'large', value: '12px' }, + { name: 'Extra Large', slug: 'extra-large', value: '16px' }, + ], + presetKey: 'padding', +}; diff --git a/packages/components/src/box-control/types.ts b/packages/components/src/box-control/types.ts index 73de68d1bd513a..43629e09258a58 100644 --- a/packages/components/src/box-control/types.ts +++ b/packages/components/src/box-control/types.ts @@ -15,6 +15,12 @@ export type CustomValueUnits = { [ key: string ]: { max: number; step: number }; }; +export interface Preset { + name: string; + slug: string; + value?: string; +} + type UnitControlPassthroughProps = Omit< UnitControlProps, 'label' | 'onChange' | 'onFocus' | 'units' @@ -94,6 +100,16 @@ export type BoxControlProps = Pick< UnitControlProps, 'units' > & * @default false */ __next40pxDefaultSize?: boolean; + /** + * Available presets to pick from. + */ + presets?: Preset[]; + /** + * The key of the preset to apply. + * If you provide a list of presets, you must provide a preset key to use. + * The format of preset selected values is going to be `var:preset|${ presetKey }|${ presetSlug }` + */ + presetKey?: string; }; export type BoxControlInputControlProps = UnitControlPassthroughProps & { @@ -120,6 +136,8 @@ export type BoxControlInputControlProps = UnitControlPassthroughProps & { * It can be a concrete side like: left, right, top, bottom or a combined one like: horizontal, vertical. */ side: keyof typeof LABELS; + presets?: Preset[]; + presetKey?: string; }; export type BoxControlIconProps = { diff --git a/packages/components/src/box-control/utils.ts b/packages/components/src/box-control/utils.ts index 111451790e35d5..26bdae4e559511 100644 --- a/packages/components/src/box-control/utils.ts +++ b/packages/components/src/box-control/utils.ts @@ -11,6 +11,7 @@ import type { BoxControlProps, BoxControlValue, CustomValueUnits, + Preset, } from './types'; import deprecated from '@wordpress/deprecated'; @@ -272,3 +273,62 @@ export function getAllowedSides( } ); return allowedSides; } + +/** + * Checks if a value is a preset value. + * + * @param value The value to check. + * @param presetKey The preset key to check against. + * @return Whether the value is a preset value. + */ +export function isValuePreset( value: string, presetKey: string ) { + return value.startsWith( `var:preset|${ presetKey }|` ); +} + +/** + * Returns the index of the preset value in the presets array. + * + * @param value The value to check. + * @param presetKey The preset key to check against. + * @param presets The array of presets to search. + * @return The index of the preset value in the presets array. + */ +export function getPresetIndexFromValue( + value: string, + presetKey: string, + presets: Preset[] +) { + if ( ! isValuePreset( value, presetKey ) ) { + return undefined; + } + + const match = value.match( + new RegExp( `^var:preset\\|${ presetKey }\\|(.+)$` ) + ); + if ( ! match ) { + return undefined; + } + const slug = match[ 1 ]; + const index = presets.findIndex( ( preset ) => { + return preset.slug === slug; + } ); + + return index !== -1 ? index : undefined; +} + +/** + * Returns the preset value from the index. + * + * @param index The index of the preset value in the presets array. + * @param presetKey The preset key to check against. + * @param presets The array of presets to search. + * @return The preset value from the index. + */ +export function getPresetValueFromIndex( + index: number, + presetKey: string, + presets: Preset[] +) { + const preset = presets[ index ]; + return `var:preset|${ presetKey }|${ preset.slug }`; +} diff --git a/packages/components/src/button-group/README.md b/packages/components/src/button-group/README.md index 5c0179d6877af9..579103dc70e062 100644 --- a/packages/components/src/button-group/README.md +++ b/packages/components/src/button-group/README.md @@ -1,5 +1,9 @@ # ButtonGroup +<div class="callout callout-alert"> + This component is deprecated. Use `ToggleGroupControl` instead. +</div> + ButtonGroup can be used to group any related buttons together. To emphasize related buttons, a group should share a common container. ![ButtonGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png) diff --git a/packages/components/src/button-group/index.tsx b/packages/components/src/button-group/index.tsx index fb2659c2a0d7de..e073b0c3b359b8 100644 --- a/packages/components/src/button-group/index.tsx +++ b/packages/components/src/button-group/index.tsx @@ -8,6 +8,7 @@ import type { ForwardedRef } from 'react'; * WordPress dependencies */ import { forwardRef } from '@wordpress/element'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -19,9 +20,16 @@ function UnforwardedButtonGroup( props: WordPressComponentProps< ButtonGroupProps, 'div', false >, ref: ForwardedRef< HTMLDivElement > ) { - const { className, ...restProps } = props; + const { className, __shouldNotWarnDeprecated, ...restProps } = props; const classes = clsx( 'components-button-group', className ); + if ( ! __shouldNotWarnDeprecated ) { + deprecated( 'wp.components.ButtonGroup', { + since: '6.8', + alternative: 'wp.components.__experimentalToggleGroupControl', + } ); + } + return ( <div ref={ ref } role="group" className={ classes } { ...restProps } /> ); @@ -31,6 +39,8 @@ function UnforwardedButtonGroup( * ButtonGroup can be used to group any related buttons together. To emphasize * related buttons, a group should share a common container. * + * @deprecated Use `ToggleGroupControl` instead. + * * ```jsx * import { Button, ButtonGroup } from '@wordpress/components'; * diff --git a/packages/components/src/button-group/stories/index.story.tsx b/packages/components/src/button-group/stories/index.story.tsx index 4b5ab3d5dfdb6b..a2df76004d4385 100644 --- a/packages/components/src/button-group/stories/index.story.tsx +++ b/packages/components/src/button-group/stories/index.story.tsx @@ -9,8 +9,15 @@ import type { Meta, StoryObj } from '@storybook/react'; import ButtonGroup from '..'; import Button from '../../button'; +/** + * ButtonGroup can be used to group any related buttons together. + * To emphasize related buttons, a group should share a common container. + * + * This component is deprecated. Use `ToggleGroupControl` instead. + */ const meta: Meta< typeof ButtonGroup > = { - title: 'Components/ButtonGroup', + title: 'Components (Deprecated)/ButtonGroup', + id: 'components-buttongroup', component: ButtonGroup, argTypes: { children: { control: false }, diff --git a/packages/components/src/button-group/types.ts b/packages/components/src/button-group/types.ts index 0bc162d5cf1c74..57388c7b5fc095 100644 --- a/packages/components/src/button-group/types.ts +++ b/packages/components/src/button-group/types.ts @@ -8,4 +8,11 @@ export type ButtonGroupProps = { * The children elements. */ children: ReactNode; + /** + * Do not throw a warning for component deprecation. + * For internal components of other components that already throw the warning. + * + * @ignore + */ + __shouldNotWarnDeprecated?: boolean; }; diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md index 99a6d0f9c24cfb..c67c795addbf4d 100644 --- a/packages/components/src/button/README.md +++ b/packages/components/src/button/README.md @@ -17,19 +17,24 @@ const Mybutton = () => ( </Button> ); ``` + ## Props ### `__next40pxDefaultSize` + - Type: `boolean` + - Required: No + - Default: `false` + Start opting into the larger default height that will become the default size in a future version. +### `accessibleWhenDisabled` + - Type: `boolean` - Required: No - Default: `false` -### `accessibleWhenDisabled` - Whether to keep the button focusable when disabled. In most cases, it is recommended to set this to `true`. Disabling a control without maintaining focusability @@ -39,111 +44,111 @@ or by preventing focus from returning to a trigger element. Learn more about the [focusability of disabled controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols) in the WAI-ARIA Authoring Practices Guide. - - Type: `boolean` - - Required: No - - Default: `false` - ### `children` -The button's children. - - Type: `ReactNode` - Required: No -### `description` +The button's children. -A visually hidden accessible description for the button. +### `description` - Type: `string` - Required: No +A visually hidden accessible description for the button. + ### `disabled` + - Type: `boolean` + - Required: No + Whether the button is disabled. If `true`, this will force a `button` element to be rendered, even when an `href` is given. In most cases, it is recommended to also set the `accessibleWhenDisabled` prop to `true`. - - Type: `boolean` - - Required: No - ### `href` -If provided, renders `a` instead of `button`. - - Type: `string` - Required: Yes -### `icon` +If provided, renders `a` instead of `button`. -If provided, renders an Icon component inside the button. +### `icon` - Type: `IconType` - Required: No -### `iconPosition` +If provided, renders an Icon component inside the button. -If provided with `icon`, sets the position of icon relative to the `text`. +### `iconPosition` - Type: `"left" | "right"` - Required: No - Default: `'left'` +If provided with `icon`, sets the position of icon relative to the `text`. + ### `iconSize` + - Type: `number` + - Required: No + If provided with `icon`, sets the icon size. Please refer to the Icon component for more details regarding the default value of its `size` prop. - - Type: `number` - - Required: No - ### `isBusy` -Indicates activity while a action is being performed. - - Type: `boolean` - Required: No -### `isDestructive` +Indicates activity while a action is being performed. -Renders a red text-based button style to indicate destructive behavior. +### `isDestructive` - Type: `boolean` - Required: No -### `isPressed` +Renders a red text-based button style to indicate destructive behavior. -Renders a pressed button style. +### `isPressed` - Type: `boolean` - Required: No -### `label` +Renders a pressed button style. -Sets the `aria-label` of the component, if none is provided. -Sets the Tooltip content if `showTooltip` is provided. +### `label` - Type: `string` - Required: No -### `shortcut` +Sets the `aria-label` of the component, if none is provided. +Sets the Tooltip content if `showTooltip` is provided. -If provided with `showTooltip`, appends the Shortcut label to the tooltip content. -If an object is provided, it should contain `display` and `ariaLabel` keys. +### `shortcut` - Type: `string | { display: string; ariaLabel: string; }` - Required: No -### `showTooltip` +If provided with `showTooltip`, appends the Shortcut label to the tooltip content. +If an object is provided, it should contain `display` and `ariaLabel` keys. -If provided, renders a Tooltip component for the button. +### `showTooltip` - Type: `boolean` - Required: No +If provided, renders a Tooltip component for the button. + ### `size` + - Type: `"small" | "default" | "compact"` + - Required: No + - Default: `'default'` + The size of the button. - `'default'`: For normal text-label buttons, unless it is a toggle button. @@ -152,34 +157,33 @@ The size of the button. If the deprecated `isSmall` prop is also defined, this prop will take precedence. - - Type: `"small" | "default" | "compact"` - - Required: No - - Default: `'default'` - ### `text` -If provided, displays the given text inside the button. If the button contains children elements, the text is displayed before them. - - Type: `string` - Required: No -### `tooltipPosition` +If provided, displays the given text inside the button. If the button contains children elements, the text is displayed before them. -If provided with `showTooltip`, sets the position of the tooltip. -Please refer to the Tooltip component for more details regarding the defaults. +### `tooltipPosition` - Type: `"top" | "middle" | "bottom" | "top center" | "top left" | "top right" | "middle center" | "middle left" | "middle right" | "bottom center" | ...` - Required: No -### `target` +If provided with `showTooltip`, sets the position of the tooltip. +Please refer to the Tooltip component for more details regarding the defaults. -If provided with `href`, sets the `target` attribute to the `a`. +### `target` - Type: `string` - Required: No +If provided with `href`, sets the `target` attribute to the `a`. + ### `variant` + - Type: `"link" | "primary" | "secondary" | "tertiary"` + - Required: No + Specifies the button's style. The accepted values are: @@ -188,6 +192,3 @@ The accepted values are: 2. `'secondary'` (the default button styles) 3. `'tertiary'` (the text-based button styles) 4. `'link'` (the link button styles) - - - Type: `"link" | "primary" | "secondary" | "tertiary"` - - Required: No diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index 61455a54e26f6b..460aeaa2781cdf 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -9,7 +9,6 @@ display: inline-flex; text-decoration: none; font-family: inherit; - font-weight: normal; font-size: $default-font-size; margin: 0; border: 0; @@ -139,8 +138,10 @@ color: $components-color-accent; background: transparent; - &:hover:not(:disabled, [aria-disabled="true"]) { - box-shadow: inset 0 0 0 $border-width $components-color-accent-darker-10; + &:hover:not(:disabled, [aria-disabled="true"], .is-pressed) { + box-shadow: inset 0 0 0 $border-width $components-color-accent-darker-20; + color: $components-color-accent-darker-20; + background: color-mix(in srgb, $components-color-accent 4%, transparent); } &:disabled:not(:focus), @@ -164,15 +165,12 @@ background: transparent; &:hover:not(:disabled, [aria-disabled="true"]) { - // TODO: Prepare for theming (https://github.com/WordPress/gutenberg/pull/45466/files#r1030872724) - /* stylelint-disable-next-line declaration-property-value-disallowed-list -- Allow tertiary buttons to use colors from the user admin color scheme. */ - background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + background: color-mix(in srgb, $components-color-accent 4%, transparent); + color: $components-color-accent-darker-20; } &:active:not(:disabled, [aria-disabled="true"]) { - // TODO: Prepare for theming (https://github.com/WordPress/gutenberg/pull/45466/files#r1030872724) - /* stylelint-disable-next-line declaration-property-value-disallowed-list -- Allow tertiary buttons to use colors from the user admin color scheme. */ - background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + background: color-mix(in srgb, $components-color-accent 8%, transparent); } // Pull left if the tertiary button stands alone after a description, so as to vertically align with items above. @@ -220,7 +218,8 @@ } } - &.is-tertiary { + &.is-tertiary, + &.is-secondary { &:hover:not(:disabled, [aria-disabled="true"]) { background: rgba($alert-red, 0.04); } diff --git a/packages/components/src/button/test/index.tsx b/packages/components/src/button/test/index.tsx index 8161e68c4e21b6..664c755ac44043 100644 --- a/packages/components/src/button/test/index.tsx +++ b/packages/components/src/button/test/index.tsx @@ -6,19 +6,26 @@ import { render, screen } from '@testing-library/react'; /** * WordPress dependencies */ -import { createRef } from '@wordpress/element'; +import { createRef, forwardRef } from '@wordpress/element'; import { plusCircle } from '@wordpress/icons'; /** * Internal dependencies */ -import Button from '..'; +import _Button from '..'; import Tooltip from '../../tooltip'; import cleanupTooltip from '../../tooltip/test/utils'; import { press } from '@ariakit/test'; jest.mock( '../../icon', () => () => <div data-testid="test-icon" /> ); +const Button = forwardRef( + ( + props: React.ComponentProps< typeof _Button >, + ref: React.ForwardedRef< unknown > + ) => <_Button __next40pxDefaultSize { ...props } ref={ ref } /> +); + describe( 'Button', () => { describe( 'basic rendering', () => { it( 'should render a button element with only one class', () => { diff --git a/packages/components/src/custom-select-control-v2/custom-select.tsx b/packages/components/src/custom-select-control-v2/custom-select.tsx index bb458abcc282ff..9c3baf182a399a 100644 --- a/packages/components/src/custom-select-control-v2/custom-select.tsx +++ b/packages/components/src/custom-select-control-v2/custom-select.tsx @@ -2,7 +2,6 @@ * External dependencies */ import * as Ariakit from '@ariakit/react'; -import { useStoreState } from '@ariakit/react'; /** * WordPress dependencies @@ -63,7 +62,7 @@ const CustomSelectButton = ( { CustomSelectStore, 'onChange' > ) => { - const { value: currentValue } = useStoreState( store ); + const { value: currentValue } = Ariakit.useStoreState( store ); const computedRenderSelectedValue = useMemo( () => renderSelectedValue ?? defaultRenderSelectedValue, diff --git a/packages/components/src/custom-select-control-v2/stories/index.story.tsx b/packages/components/src/custom-select-control-v2/stories/index.story.tsx index 75226314af8dbd..b65c599ec9997c 100644 --- a/packages/components/src/custom-select-control-v2/stories/index.story.tsx +++ b/packages/components/src/custom-select-control-v2/stories/index.story.tsx @@ -2,6 +2,7 @@ * External dependencies */ import type { Meta, StoryFn } from '@storybook/react'; +import { fn } from '@storybook/test'; /** * WordPress dependencies @@ -44,6 +45,9 @@ const meta: Meta< typeof CustomSelectControlV2 > = { </div> ), ], + args: { + onChange: fn(), + }, }; export default meta; diff --git a/packages/components/src/custom-select-control/index.tsx b/packages/components/src/custom-select-control/index.tsx index 339944f4198722..e014e4bc642eef 100644 --- a/packages/components/src/custom-select-control/index.tsx +++ b/packages/components/src/custom-select-control/index.tsx @@ -149,16 +149,16 @@ function CustomSelectControl< T extends CustomSelectOption >( ); } ); - const { value: currentValue } = store.getState(); + const currentValue = Ariakit.useStoreState( store, 'value' ); const renderSelectedValueHint = () => { const selectedOptionHint = options ?.map( applyOptionDeprecations ) - ?.find( ( { name } ) => store.getState().value === name )?.hint; + ?.find( ( { name } ) => currentValue === name )?.hint; return ( <Styled.SelectedExperimentalHintWrapper> - { store.getState().value } + { currentValue } { selectedOptionHint && ( <Styled.SelectedExperimentalHintItem // Keeping the classname for legacy reasons diff --git a/packages/components/src/date-time/date/index.tsx b/packages/components/src/date-time/date/index.tsx index ca093f9d70847b..e7afcccf249dc0 100644 --- a/packages/components/src/date-time/date/index.tsx +++ b/packages/components/src/date-time/date/index.tsx @@ -306,6 +306,7 @@ function Day( { return ( <DayButton + __next40pxDefaultSize ref={ ref } className="components-datetime__date__day" // Unused, for backwards compatibility. disabled={ isInvalid } diff --git a/packages/components/src/dimension-control/index.tsx b/packages/components/src/dimension-control/index.tsx index 2cc39379ebde0c..ffdfaeb84ee51d 100644 --- a/packages/components/src/dimension-control/index.tsx +++ b/packages/components/src/dimension-control/index.tsx @@ -113,6 +113,7 @@ export function DimensionControl( props: DimensionControlProps ) { <ContextSystemProvider value={ CONTEXT_VALUE }> <SelectControl __next40pxDefaultSize={ __next40pxDefaultSize } + __shouldNotWarnDeprecated36pxSize __nextHasNoMarginBottom={ __nextHasNoMarginBottom } className={ clsx( className, diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap index fd6cc2df3fcde7..b1adfd5d9221ab 100644 --- a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap +++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap @@ -63,7 +63,7 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` } .emotion-12 { - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); line-height: 1.4; margin: 0; text-wrap: balance; @@ -345,7 +345,7 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` } .emotion-12 { - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); line-height: 1.4; margin: 0; text-wrap: balance; @@ -637,7 +637,7 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] } .emotion-12 { - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); line-height: 1.4; margin: 0; text-wrap: balance; @@ -941,7 +941,7 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] } .emotion-12 { - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); line-height: 1.4; margin: 0; text-wrap: balance; diff --git a/packages/components/src/disabled/stories/index.story.tsx b/packages/components/src/disabled/stories/index.story.tsx index f017305507814e..591118681a82de 100644 --- a/packages/components/src/disabled/stories/index.story.tsx +++ b/packages/components/src/disabled/stories/index.story.tsx @@ -55,6 +55,7 @@ const Form = () => { /> <SelectControl __nextHasNoMarginBottom + __next40pxDefaultSize label="Select Control" onChange={ () => {} } options={ [ diff --git a/packages/components/src/drop-zone/stories/index.story.tsx b/packages/components/src/drop-zone/stories/index.story.tsx index 7e2dcbf03c03b1..fe0be94e74fe85 100644 --- a/packages/components/src/drop-zone/stories/index.story.tsx +++ b/packages/components/src/drop-zone/stories/index.story.tsx @@ -21,7 +21,13 @@ export default meta; const Template: StoryFn< typeof DropZone > = ( props ) => { return ( - <div style={ { background: 'lightgray', padding: 16 } }> + <div + style={ { + background: 'lightgray', + padding: 32, + position: 'relative', + } } + > Drop something here <DropZone { ...props } /> </div> diff --git a/packages/components/src/font-size-picker/styles.ts b/packages/components/src/font-size-picker/styles.ts index f47ca41b51eb71..b0e33b5aea3a2e 100644 --- a/packages/components/src/font-size-picker/styles.ts +++ b/packages/components/src/font-size-picker/styles.ts @@ -16,6 +16,7 @@ export const Container = styled.fieldset` border: 0; margin: 0; padding: 0; + display: contents; `; export const Header = styled( HStack )` diff --git a/packages/components/src/form-file-upload/README.md b/packages/components/src/form-file-upload/README.md index c6a7205815de53..74e6e369383383 100644 --- a/packages/components/src/form-file-upload/README.md +++ b/packages/components/src/form-file-upload/README.md @@ -19,60 +19,64 @@ const MyFormFileUpload = () => ( </FormFileUpload> ); ``` + ## Props ### `__next40pxDefaultSize` -Start opting into the larger default height that will become the default size in a future version. - - Type: `boolean` - Required: No - Default: `false` +Start opting into the larger default height that will become the default size in a future version. + ### `accept` + - Type: `string` + - Required: No + A string passed to the `input` element that tells the browser which [file types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#Unique_file_type_specifiers) can be uploaded by the user. e.g: `image/*,video/*`. - - Type: `string` - - Required: No - ### `children` -Children are passed as children of `Button`. - - Type: `ReactNode` - Required: No +Children are passed as children of `Button`. + ### `icon` + - Type: `IconType` + - Required: No + The icon to render in the default button. See the `Icon` component docs for more information. - - Type: `IconType` - - Required: No - ### `multiple` -Whether to allow multiple selection of files or not. - - Type: `boolean` - Required: No - Default: `false` +Whether to allow multiple selection of files or not. + ### `onChange` + - Type: `ChangeEventHandler<HTMLInputElement>` + - Required: Yes + Callback function passed directly to the `input` file element. Select files will be available in `event.currentTarget.files`. - - Type: `ChangeEventHandler<HTMLInputElement>` - - Required: Yes - ### `onClick` + - Type: `MouseEventHandler<HTMLInputElement>` + - Required: No + Callback function passed directly to the `input` file element. This can be useful when you want to force a `change` event to fire when @@ -89,17 +93,14 @@ an empty string in the `onClick` function. </FormFileUpload> ``` - - Type: `MouseEventHandler<HTMLInputElement>` - - Required: No - ### `render` + - Type: `(arg: { openFileDialog: () => void; }) => ReactNode` + - Required: No + Optional callback function used to render the UI. If passed, the component does not render the default UI (a button) and calls this function to render it. The function receives an object with property `openFileDialog`, a function that, when called, opens the browser native file upload modal window. - - - Type: `(arg: { openFileDialog: () => void; }) => ReactNode` - - Required: No diff --git a/packages/components/src/gradient-picker/README.md b/packages/components/src/gradient-picker/README.md index ec0210d03c0a43..275c46ec5958c9 100644 --- a/packages/components/src/gradient-picker/README.md +++ b/packages/components/src/gradient-picker/README.md @@ -43,114 +43,115 @@ const MyGradientPicker = () => { ); }; ``` + ## Props ### `__experimentalIsRenderedInSidebar` -Whether this is rendered in the sidebar. - - Type: `boolean` - Required: No - Default: `false` -### `asButtons` +Whether this is rendered in the sidebar. -Whether the control should present as a set of buttons, -each with its own tab stop. +### `asButtons` - Type: `boolean` - Required: No - Default: `false` -### `aria-label` +Whether the control should present as a set of buttons, +each with its own tab stop. -A label to identify the purpose of the control. +### `aria-label` - Type: `string` - Required: No -### `aria-labelledby` +A label to identify the purpose of the control. -An ID of an element to provide a label for the control. +### `aria-labelledby` - Type: `string` - Required: No -### `className` +An ID of an element to provide a label for the control. -The class name added to the wrapper. +### `className` - Type: `string` - Required: No -### `clearable` +The class name added to the wrapper. -Whether the palette should have a clearing button or not. +### `clearable` - Type: `boolean` - Required: No - Default: `true` -### `disableCustomGradients` +Whether the palette should have a clearing button or not. -If true, the gradient picker will not be displayed and only defined -gradients from `gradients` will be shown. +### `disableCustomGradients` - Type: `boolean` - Required: No - Default: `false` -### `enableAlpha` +If true, the gradient picker will not be displayed and only defined +gradients from `gradients` will be shown. -Whether to enable alpha transparency options in the picker. +### `enableAlpha` - Type: `boolean` - Required: No - Default: `true` +Whether to enable alpha transparency options in the picker. + ### `gradients` + - Type: `GradientsProp` + - Required: No + - Default: `[]` + An array of objects as predefined gradients displayed above the gradient selector. Alternatively, if there are multiple sets (or 'origins') of gradients, you can pass an array of objects each with a `name` and a `gradients` array which will in turn contain the predefined gradient objects. - - Type: `GradientsProp` - - Required: No - - Default: `[]` - ### `headingLevel` -The heading level. Only applies in cases where gradients are provided -from multiple origins (i.e. when the array passed as the `gradients` prop -contains two or more items). - - Type: `1 | 2 | 3 | 4 | 5 | 6 | "1" | "2" | "3" | "4" | ...` - Required: No - Default: `2` -### `loop` +The heading level. Only applies in cases where gradients are provided +from multiple origins (i.e. when the array passed as the `gradients` prop +contains two or more items). -Prevents keyboard interaction from wrapping around. -Only used when `asButtons` is not true. +### `loop` - Type: `boolean` - Required: No - Default: `true` -### `onChange` +Prevents keyboard interaction from wrapping around. +Only used when `asButtons` is not true. -The function called when a new gradient has been defined. It is passed to -the `currentGradient` as an argument. +### `onChange` - Type: `(currentGradient: string) => void` - Required: Yes -### `value` +The function called when a new gradient has been defined. It is passed to +the `currentGradient` as an argument. -The current value of the gradient. Pass a css gradient string (See default value for example). -Optionally pass in a `null` value to specify no gradient is currently selected. +### `value` - Type: `string` - Required: No - Default: `'linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%)'` + +The current value of the gradient. Pass a css gradient string (See default value for example). +Optionally pass in a `null` value to specify no gradient is currently selected. diff --git a/packages/components/src/heading/hook.ts b/packages/components/src/heading/hook.ts index d242afe1fdb2f5..132595d69c4f76 100644 --- a/packages/components/src/heading/hook.ts +++ b/packages/components/src/heading/hook.ts @@ -14,7 +14,7 @@ export function useHeading( const { as: asProp, level = 2, - color = COLORS.gray[ 900 ], + color = COLORS.theme.foreground, isBlock = true, weight = CONFIG.fontWeightHeading as import('react').CSSProperties[ 'fontWeight' ], ...otherProps diff --git a/packages/components/src/heading/test/__snapshots__/index.tsx.snap b/packages/components/src/heading/test/__snapshots__/index.tsx.snap index cf863c4b2bb2ef..675810948404fe 100644 --- a/packages/components/src/heading/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/heading/test/__snapshots__/index.tsx.snap @@ -2,12 +2,12 @@ exports[`props should render correctly 1`] = ` .emotion-0 { - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); line-height: 1.4; margin: 0; text-wrap: balance; text-wrap: pretty; - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); font-size: calc(1.95 * 13px); font-weight: 600; display: block; @@ -30,7 +30,7 @@ Snapshot Diff: @@ -1,10 +1,10 @@ Array [ Object { - "color": "#1e1e1e", + "color": "var(--wp-components-color-foreground, #1e1e1e)", "display": "block", - "font-size": "calc(1.25 * 13px)", + "font-size": "calc(1.95 * 13px)", @@ -49,7 +49,7 @@ Snapshot Diff: @@ -1,10 +1,10 @@ Array [ Object { - "color": "#1e1e1e", + "color": "var(--wp-components-color-foreground, #1e1e1e)", "display": "block", - "font-size": "calc(1.25 * 13px)", + "font-size": "calc(1.95 * 13px)", diff --git a/packages/components/src/icon/README.md b/packages/components/src/icon/README.md index 63d52c1fd20b13..2c9726dbcf5418 100644 --- a/packages/components/src/icon/README.md +++ b/packages/components/src/icon/README.md @@ -11,10 +11,15 @@ import { wordpress } from '@wordpress/icons'; <Icon icon={ wordpress } /> ``` + ## Props ### `icon` + - Type: `IconType` + - Required: No + - Default: `null` + The icon to render. In most cases, you should use an icon from [the `@wordpress/icons` package](https://wordpress.github.io/gutenberg/?path=/story/icons-icon--library). @@ -24,16 +29,12 @@ Other supported values are: component instances, functions, The `size` value, as well as any other additional props, will be passed through. - - Type: `IconType` - - Required: No - - Default: `null` - ### `size` -The size (width and height) of the icon. - -Defaults to `20` when `icon` is a string (i.e. a Dashicon id), otherwise `24`. - - Type: `number` - Required: No - Default: `'string' === typeof icon ? 20 : 24` + +The size (width and height) of the icon. + +Defaults to `20` when `icon` is a string (i.e. a Dashicon id), otherwise `24`. diff --git a/packages/components/src/input-control/README.md b/packages/components/src/input-control/README.md index 58a3b4a3b1a094..ff5c70decebeb7 100644 --- a/packages/components/src/input-control/README.md +++ b/packages/components/src/input-control/README.md @@ -17,6 +17,7 @@ const Example = () => { return ( <InputControl + __next40pxDefaultSize value={ value } onChange={ ( nextValue ) => setValue( nextValue ?? '' ) } /> diff --git a/packages/components/src/input-control/index.tsx b/packages/components/src/input-control/index.tsx index fd0fc0a5c45536..d346d1b31b1118 100644 --- a/packages/components/src/input-control/index.tsx +++ b/packages/components/src/input-control/index.tsx @@ -20,6 +20,7 @@ import { space } from '../utils/space'; import { useDraft } from './utils'; import BaseControl from '../base-control'; import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props'; +import { maybeWarnDeprecated36pxSize } from '../utils/deprecated-36px-size'; const noop = () => {}; @@ -36,6 +37,7 @@ export function UnforwardedInputControl( ) { const { __next40pxDefaultSize, + __shouldNotWarnDeprecated36pxSize, __unstableStateReducer: stateReducer = ( state ) => state, __unstableInputWidth, className, @@ -68,6 +70,13 @@ export function UnforwardedInputControl( const helpProp = !! help ? { 'aria-describedby': `${ id }__help` } : {}; + maybeWarnDeprecated36pxSize( { + componentName: 'InputControl', + __next40pxDefaultSize, + size, + __shouldNotWarnDeprecated36pxSize, + } ); + return ( <BaseControl className={ classes } @@ -125,6 +134,7 @@ export function UnforwardedInputControl( * * return ( * <InputControl + * __next40pxDefaultSize * value={ value } * onChange={ ( nextValue ) => setValue( nextValue ?? '' ) } * /> diff --git a/packages/components/src/input-control/stories/index.story.tsx b/packages/components/src/input-control/stories/index.story.tsx index 136301c42e7d09..40630938dbb370 100644 --- a/packages/components/src/input-control/stories/index.story.tsx +++ b/packages/components/src/input-control/stories/index.story.tsx @@ -46,6 +46,7 @@ export const Default = Template.bind( {} ); Default.args = { label: 'Value', placeholder: 'Placeholder', + __next40pxDefaultSize: true, }; export const WithHelpText = Template.bind( {} ); @@ -117,7 +118,6 @@ export const ShowPassword: StoryFn< typeof InputControl > = ( args ) => { return ( <InputControl type={ visible ? 'text' : 'password' } - label="Password" suffix={ <InputControlSuffixWrapper variant="control"> <Button @@ -132,3 +132,8 @@ export const ShowPassword: StoryFn< typeof InputControl > = ( args ) => { /> ); }; +ShowPassword.args = { + ...Default.args, + label: 'Password', + placeholder: undefined, +}; diff --git a/packages/components/src/input-control/test/index.js b/packages/components/src/input-control/test/index.js index ace3086c388c8b..46332eb6eea704 100644 --- a/packages/components/src/input-control/test/index.js +++ b/packages/components/src/input-control/test/index.js @@ -17,9 +17,15 @@ import BaseInputControl from '../'; const getInput = () => screen.getByTestId( 'input' ); describe( 'InputControl', () => { - const InputControl = ( props ) => ( - <BaseInputControl { ...props } data-testid="input" /> - ); + const InputControl = ( props ) => { + return ( + <BaseInputControl + { ...props } + __next40pxDefaultSize + data-testid="input" + /> + ); + }; describe( 'Basic rendering', () => { it( 'should render', () => { diff --git a/packages/components/src/input-control/types.ts b/packages/components/src/input-control/types.ts index 13f078cd89cc1f..edb69def619057 100644 --- a/packages/components/src/input-control/types.ts +++ b/packages/components/src/input-control/types.ts @@ -40,6 +40,13 @@ interface BaseProps { * @default false */ __next40pxDefaultSize?: boolean; + /** + * Do not throw a warning for the deprecated 36px default size. + * For internal components of other components that already throw the warning. + * + * @ignore + */ + __shouldNotWarnDeprecated36pxSize?: boolean; __unstableInputWidth?: CSSProperties[ 'width' ]; /** * If true, the label will only be visible to screen readers. @@ -129,7 +136,7 @@ export interface InputBaseProps extends BaseProps, FlexProps { * If you want to apply standard padding in accordance with the size variant, wrap the element in * the provided `<InputControlPrefixWrapper>` component. * - * @example + * ```jsx * import { * __experimentalInputControl as InputControl, * __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, @@ -138,6 +145,7 @@ export interface InputBaseProps extends BaseProps, FlexProps { * <InputControl * prefix={<InputControlPrefixWrapper>@</InputControlPrefixWrapper>} * /> + * ``` */ prefix?: ReactNode; /** @@ -147,7 +155,7 @@ export interface InputBaseProps extends BaseProps, FlexProps { * If you want to apply standard padding in accordance with the size variant, wrap the element in * the provided `<InputControlSuffixWrapper>` component. * - * @example + * ```jsx * import { * __experimentalInputControl as InputControl, * __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper, @@ -156,6 +164,7 @@ export interface InputBaseProps extends BaseProps, FlexProps { * <InputControl * suffix={<InputControlSuffixWrapper>%</InputControlSuffixWrapper>} * /> + * ``` */ suffix?: ReactNode; /** diff --git a/packages/components/src/menu/checkbox-item.tsx b/packages/components/src/menu/checkbox-item.tsx index ddb700b43324a6..69339387c3add5 100644 --- a/packages/components/src/menu/checkbox-item.tsx +++ b/packages/components/src/menu/checkbox-item.tsx @@ -21,7 +21,7 @@ export const MenuCheckboxItem = forwardRef< HTMLDivElement, WordPressComponentProps< MenuCheckboxItemProps, 'div', false > >( function MenuCheckboxItem( - { suffix, children, hideOnClick = false, ...props }, + { suffix, children, disabled = false, hideOnClick = false, ...props }, ref ) { const menuContext = useContext( MenuContext ); @@ -37,6 +37,7 @@ export const MenuCheckboxItem = forwardRef< ref={ ref } { ...props } accessibleWhenDisabled + disabled={ disabled } hideOnClick={ hideOnClick } store={ menuContext.store } > diff --git a/packages/components/src/menu/index.tsx b/packages/components/src/menu/index.tsx index 6f6e89b0a1c72b..2e0fc91cfbc34f 100644 --- a/packages/components/src/menu/index.tsx +++ b/packages/components/src/menu/index.tsx @@ -2,28 +2,18 @@ * External dependencies */ import * as Ariakit from '@ariakit/react'; -import { useStoreState } from '@ariakit/react'; /** * WordPress dependencies */ -import { - useContext, - useMemo, - cloneElement, - isValidElement, - useCallback, -} from '@wordpress/element'; -import { isRTL } from '@wordpress/i18n'; -import { chevronRightSmall } from '@wordpress/icons'; +import { useContext, useMemo } from '@wordpress/element'; +import { isRTL as isRTLFn } from '@wordpress/i18n'; /** * Internal dependencies */ -import { useContextSystem, contextConnect } from '../context'; -import type { WordPressComponentProps } from '../context'; +import { useContextSystem, contextConnectWithoutRef } from '../context'; import type { MenuContext as MenuContextType, MenuProps } from './types'; -import * as Styled from './styles'; import { MenuContext } from './context'; import { MenuItem } from './item'; import { MenuCheckboxItem } from './checkbox-item'; @@ -33,49 +23,36 @@ import { MenuGroupLabel } from './group-label'; import { MenuSeparator } from './separator'; import { MenuItemLabel } from './item-label'; import { MenuItemHelpText } from './item-help-text'; +import { MenuTriggerButton } from './trigger-button'; +import { MenuSubmenuTriggerItem } from './submenu-trigger-item'; +import { MenuPopover } from './popover'; -const UnconnectedMenu = ( - props: WordPressComponentProps< MenuProps, 'div', false >, - ref: React.ForwardedRef< HTMLDivElement > -) => { +const UnconnectedMenu = ( props: MenuProps ) => { const { - // Store props - open, + children, defaultOpen = false, + open, onOpenChange, placement, - // Menu trigger props - trigger, - - // Menu props - gutter, - children, - shift, - modal = true, - // From internal components context variant, - - // Rest - ...otherProps - } = useContextSystem< typeof props & Pick< MenuContextType, 'variant' > >( - props, - 'Menu' - ); + } = useContextSystem< + // @ts-expect-error TODO: missing 'className' in MenuProps + typeof props & Pick< MenuContextType, 'variant' > + >( props, 'Menu' ); const parentContext = useContext( MenuContext ); - const computedDirection = isRTL() ? 'rtl' : 'ltr'; + const rtl = isRTLFn(); // If an explicit value for the `placement` prop is not passed, // apply a default placement of `bottom-start` for the root menu popover, // and of `right-start` for nested menu popovers. let computedPlacement = - props.placement ?? - ( parentContext?.store ? 'right-start' : 'bottom-start' ); + placement ?? ( parentContext?.store ? 'right-start' : 'bottom-start' ); // Swap left/right in case of RTL direction - if ( computedDirection === 'rtl' ) { + if ( rtl ) { if ( /right/.test( computedPlacement ) ) { computedPlacement = computedPlacement.replace( 'right', @@ -98,7 +75,7 @@ const UnconnectedMenu = ( setOpen( willBeOpen ) { onOpenChange?.( willBeOpen ); }, - rtl: computedDirection === 'rtl', + rtl, } ); const contextValue = useMemo( @@ -106,134 +83,53 @@ const UnconnectedMenu = ( [ menuStore, variant ] ); - // Extract the side from the applied placement — useful for animations. - // Using `currentPlacement` instead of `placement` to make sure that we - // use the final computed placement (including "flips" etc). - const appliedPlacementSide = useStoreState( - menuStore, - 'currentPlacement' - ).split( '-' )[ 0 ]; - - if ( - menuStore.parent && - ! ( isValidElement( trigger ) && MenuItem === trigger.type ) - ) { - // eslint-disable-next-line no-console - console.warn( - 'For nested Menus, the `trigger` should always be a `MenuItem`.' - ); - } - - const hideOnEscape = useCallback( - ( event: React.KeyboardEvent< Element > ) => { - // Pressing Escape can cause unexpected consequences (ie. exiting - // full screen mode on MacOs, close parent modals...). - event.preventDefault(); - // Returning `true` causes the menu to hide. - return true; - }, - [] - ); - - const wrapperProps = useMemo( - () => ( { - dir: computedDirection, - style: { - direction: - computedDirection as React.CSSProperties[ 'direction' ], - }, - } ), - [ computedDirection ] - ); - return ( - <> - { /* Menu trigger */ } - <Ariakit.MenuButton - ref={ ref } - store={ menuStore } - render={ - menuStore.parent - ? cloneElement( trigger, { - // Add submenu arrow, unless a `suffix` is explicitly specified - suffix: ( - <> - { trigger.props.suffix } - <Styled.SubmenuChevronIcon - aria-hidden="true" - icon={ chevronRightSmall } - size={ 24 } - preserveAspectRatio="xMidYMid slice" - /> - </> - ), - } ) - : trigger - } - /> - - { /* Menu popover */ } - <Ariakit.Menu - { ...otherProps } - modal={ modal } - store={ menuStore } - // Root menu has an 8px distance from its trigger, - // otherwise 0 (which causes the submenu to slightly overlap) - gutter={ gutter ?? ( menuStore.parent ? 0 : 8 ) } - // Align nested menu by the same (but opposite) amount - // as the menu container's padding. - shift={ shift ?? ( menuStore.parent ? -4 : 0 ) } - hideOnHoverOutside={ false } - data-side={ appliedPlacementSide } - wrapperProps={ wrapperProps } - hideOnEscape={ hideOnEscape } - unmountOnHide - render={ ( renderProps ) => ( - // Two wrappers are needed for the entry animation, where the menu - // container scales with a different factor than its contents. - // The {...renderProps} are passed to the inner wrapper, so that the - // menu element is the direct parent of the menu item elements. - <Styled.MenuPopoverOuterWrapper variant={ variant }> - <Styled.MenuPopoverInnerWrapper { ...renderProps } /> - </Styled.MenuPopoverOuterWrapper> - ) } - > - <MenuContext.Provider value={ contextValue }> - { children } - </MenuContext.Provider> - </Ariakit.Menu> - </> + <MenuContext.Provider value={ contextValue }> + { children } + </MenuContext.Provider> ); }; -export const Menu = Object.assign( contextConnect( UnconnectedMenu, 'Menu' ), { - Context: Object.assign( MenuContext, { - displayName: 'Menu.Context', - } ), - Item: Object.assign( MenuItem, { - displayName: 'Menu.Item', - } ), - RadioItem: Object.assign( MenuRadioItem, { - displayName: 'Menu.RadioItem', - } ), - CheckboxItem: Object.assign( MenuCheckboxItem, { - displayName: 'Menu.CheckboxItem', - } ), - Group: Object.assign( MenuGroup, { - displayName: 'Menu.Group', - } ), - GroupLabel: Object.assign( MenuGroupLabel, { - displayName: 'Menu.GroupLabel', - } ), - Separator: Object.assign( MenuSeparator, { - displayName: 'Menu.Separator', - } ), - ItemLabel: Object.assign( MenuItemLabel, { - displayName: 'Menu.ItemLabel', - } ), - ItemHelpText: Object.assign( MenuItemHelpText, { - displayName: 'Menu.ItemHelpText', - } ), -} ); +export const Menu = Object.assign( + contextConnectWithoutRef( UnconnectedMenu, 'Menu' ), + { + Context: Object.assign( MenuContext, { + displayName: 'Menu.Context', + } ), + Item: Object.assign( MenuItem, { + displayName: 'Menu.Item', + } ), + RadioItem: Object.assign( MenuRadioItem, { + displayName: 'Menu.RadioItem', + } ), + CheckboxItem: Object.assign( MenuCheckboxItem, { + displayName: 'Menu.CheckboxItem', + } ), + Group: Object.assign( MenuGroup, { + displayName: 'Menu.Group', + } ), + GroupLabel: Object.assign( MenuGroupLabel, { + displayName: 'Menu.GroupLabel', + } ), + Separator: Object.assign( MenuSeparator, { + displayName: 'Menu.Separator', + } ), + ItemLabel: Object.assign( MenuItemLabel, { + displayName: 'Menu.ItemLabel', + } ), + ItemHelpText: Object.assign( MenuItemHelpText, { + displayName: 'Menu.ItemHelpText', + } ), + Popover: Object.assign( MenuPopover, { + displayName: 'Menu.Popover', + } ), + TriggerButton: Object.assign( MenuTriggerButton, { + displayName: 'Menu.TriggerButton', + } ), + SubmenuTriggerItem: Object.assign( MenuSubmenuTriggerItem, { + displayName: 'Menu.SubmenuTriggerItem', + } ), + } +); export default Menu; diff --git a/packages/components/src/menu/item.tsx b/packages/components/src/menu/item.tsx index 6d09bdf3d0f591..a716cbcc89654c 100644 --- a/packages/components/src/menu/item.tsx +++ b/packages/components/src/menu/item.tsx @@ -15,7 +15,15 @@ export const MenuItem = forwardRef< HTMLDivElement, WordPressComponentProps< MenuItemProps, 'div', false > >( function MenuItem( - { prefix, suffix, children, hideOnClick = true, ...props }, + { + prefix, + suffix, + children, + disabled = false, + hideOnClick = true, + store, + ...props + }, ref ) { const menuContext = useContext( MenuContext ); @@ -26,13 +34,20 @@ export const MenuItem = forwardRef< ); } + // In most cases, the menu store will be retrieved from context (ie. the store + // created by the top-level menu component). But in rare cases (ie. + // `Menu.SubmenuTriggerItem`), the context store wouldn't be correct. This is + // why the component accepts a `store` prop to override the context store. + const computedStore = store ?? menuContext.store; + return ( <Styled.MenuItem ref={ ref } { ...props } accessibleWhenDisabled + disabled={ disabled } hideOnClick={ hideOnClick } - store={ menuContext.store } + store={ computedStore } > <Styled.ItemPrefixWrapper>{ prefix }</Styled.ItemPrefixWrapper> diff --git a/packages/components/src/menu/popover.tsx b/packages/components/src/menu/popover.tsx new file mode 100644 index 00000000000000..19972a31027ce1 --- /dev/null +++ b/packages/components/src/menu/popover.tsx @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { + useContext, + useMemo, + forwardRef, + useCallback, +} from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import type { MenuPopoverProps } from './types'; +import * as Styled from './styles'; +import { MenuContext } from './context'; + +export const MenuPopover = forwardRef< + HTMLDivElement, + WordPressComponentProps< MenuPopoverProps, 'div', false > +>( function MenuPopover( + { gutter, children, shift, modal = true, ...otherProps }, + ref +) { + const menuContext = useContext( MenuContext ); + + // Extract the side from the applied placement — useful for animations. + // Using `currentPlacement` instead of `placement` to make sure that we + // use the final computed placement (including "flips" etc). + const appliedPlacementSide = Ariakit.useStoreState( + menuContext?.store, + 'currentPlacement' + )?.split( '-' )[ 0 ]; + + const hideOnEscape = useCallback( + ( event: React.KeyboardEvent< Element > ) => { + // Pressing Escape can cause unexpected consequences (ie. exiting + // full screen mode on MacOs, close parent modals...). + event.preventDefault(); + // Returning `true` causes the menu to hide. + return true; + }, + [] + ); + + const computedDirection = Ariakit.useStoreState( menuContext?.store, 'rtl' ) + ? 'rtl' + : 'ltr'; + + const wrapperProps = useMemo( + () => ( { + dir: computedDirection, + style: { + direction: + computedDirection as React.CSSProperties[ 'direction' ], + }, + } ), + [ computedDirection ] + ); + + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.Popover can only be rendered inside a Menu component' + ); + } + + return ( + <Ariakit.Menu + { ...otherProps } + ref={ ref } + modal={ modal } + store={ menuContext.store } + // Root menu has an 8px distance from its trigger, + // otherwise 0 (which causes the submenu to slightly overlap) + gutter={ gutter ?? ( menuContext.store.parent ? 0 : 8 ) } + // Align nested menu by the same (but opposite) amount + // as the menu container's padding. + shift={ shift ?? ( menuContext.store.parent ? -4 : 0 ) } + hideOnHoverOutside={ false } + data-side={ appliedPlacementSide } + wrapperProps={ wrapperProps } + hideOnEscape={ hideOnEscape } + unmountOnHide + render={ ( renderProps ) => ( + // Two wrappers are needed for the entry animation, where the menu + // container scales with a different factor than its contents. + // The {...renderProps} are passed to the inner wrapper, so that the + // menu element is the direct parent of the menu item elements. + <Styled.MenuPopoverOuterWrapper variant={ menuContext.variant }> + <Styled.MenuPopoverInnerWrapper { ...renderProps } /> + </Styled.MenuPopoverOuterWrapper> + ) } + > + { children } + </Ariakit.Menu> + ); +} ); diff --git a/packages/components/src/menu/radio-item.tsx b/packages/components/src/menu/radio-item.tsx index 5534a6b7f3e10c..28b3199d7d36b8 100644 --- a/packages/components/src/menu/radio-item.tsx +++ b/packages/components/src/menu/radio-item.tsx @@ -28,7 +28,7 @@ export const MenuRadioItem = forwardRef< HTMLDivElement, WordPressComponentProps< MenuRadioItemProps, 'div', false > >( function MenuRadioItem( - { suffix, children, hideOnClick = false, ...props }, + { suffix, children, disabled = false, hideOnClick = false, ...props }, ref ) { const menuContext = useContext( MenuContext ); @@ -44,6 +44,7 @@ export const MenuRadioItem = forwardRef< ref={ ref } { ...props } accessibleWhenDisabled + disabled={ disabled } hideOnClick={ hideOnClick } store={ menuContext.store } > diff --git a/packages/components/src/menu/stories/index.story.tsx b/packages/components/src/menu/stories/index.story.tsx index ad4794057e0e03..37ebb6f905dc84 100644 --- a/packages/components/src/menu/stories/index.story.tsx +++ b/packages/components/src/menu/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { Meta, StoryFn } from '@storybook/react'; +import type { StoryObj, Meta } from '@storybook/react'; import { css } from '@emotion/react'; /** @@ -20,6 +20,7 @@ import Button from '../../button'; import Modal from '../../modal'; import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill'; import { ContextSystemProvider } from '../../context'; +import type { MenuProps } from '../types'; const meta: Meta< typeof Menu > = { id: 'components-experimental-menu', @@ -44,10 +45,15 @@ const meta: Meta< typeof Menu > = { ItemLabel: Menu.ItemLabel, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 ItemHelpText: Menu.ItemHelpText, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + TriggerButton: Menu.TriggerButton, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + SubmenuTriggerItem: Menu.SubmenuTriggerItem, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + Popover: Menu.Popover, }, argTypes: { children: { control: false }, - trigger: { control: false }, }, tags: [ 'status-private' ], parameters: { @@ -61,259 +67,341 @@ const meta: Meta< typeof Menu > = { }; export default meta; -export const Default: StoryFn< typeof Menu > = ( props ) => ( - <Menu { ...props }> - <Menu.Item> - <Menu.ItemLabel>Label</Menu.ItemLabel> - </Menu.Item> - <Menu.Item> - <Menu.ItemLabel>Label</Menu.ItemLabel> - <Menu.ItemHelpText>Help text</Menu.ItemHelpText> - </Menu.Item> - <Menu.Item> - <Menu.ItemLabel>Label</Menu.ItemLabel> - <Menu.ItemHelpText> - The menu item help text is automatically truncated when there - are more than two lines of text - </Menu.ItemHelpText> - </Menu.Item> - <Menu.Item hideOnClick={ false }> - <Menu.ItemLabel>Label</Menu.ItemLabel> - <Menu.ItemHelpText> - This item doesn&apos;t close the menu on click - </Menu.ItemHelpText> - </Menu.Item> - <Menu.Item disabled>Disabled item</Menu.Item> - <Menu.Separator /> - <Menu.Group> - <Menu.GroupLabel>Group label</Menu.GroupLabel> - <Menu.Item prefix={ <Icon icon={ customLink } size={ 24 } /> }> - <Menu.ItemLabel>With prefix</Menu.ItemLabel> - </Menu.Item> - <Menu.Item suffix="⌘S">With suffix</Menu.Item> - <Menu.Item - disabled - prefix={ <Icon icon={ formatCapitalize } size={ 24 } /> } - suffix="⌥⌘T" - > - <Menu.ItemLabel>Disabled with prefix and suffix</Menu.ItemLabel> - <Menu.ItemHelpText>And help text</Menu.ItemHelpText> - </Menu.Item> - </Menu.Group> - </Menu> -); -Default.args = { - trigger: ( - <Button __next40pxDefaultSize variant="secondary"> - Open menu - </Button> - ), -}; - -export const WithSubmenu: StoryFn< typeof Menu > = ( props ) => ( - <Menu { ...props }> - <Menu.Item>Level 1 item</Menu.Item> - <Menu - trigger={ - <Menu.Item suffix="Suffix"> - <Menu.ItemLabel> - Submenu trigger item with a long label - </Menu.ItemLabel> - </Menu.Item> - } - > - <Menu.Item> - <Menu.ItemLabel>Level 2 item</Menu.ItemLabel> - </Menu.Item> - <Menu.Item> - <Menu.ItemLabel>Level 2 item</Menu.ItemLabel> - </Menu.Item> - <Menu - trigger={ +export const Default: StoryObj< typeof Menu > = { + args: { + children: ( + <> + <Menu.TriggerButton + render={ + <Button __next40pxDefaultSize variant="secondary" /> + } + > + Open menu + </Menu.TriggerButton> + <Menu.Popover> <Menu.Item> - <Menu.ItemLabel>Submenu trigger</Menu.ItemLabel> + <Menu.ItemLabel>Label</Menu.ItemLabel> </Menu.Item> - } - > - <Menu.Item> - <Menu.ItemLabel>Level 3 item</Menu.ItemLabel> - </Menu.Item> - <Menu.Item> - <Menu.ItemLabel>Level 3 item</Menu.ItemLabel> - </Menu.Item> - </Menu> - </Menu> - </Menu> -); -WithSubmenu.args = { - ...Default.args, + <Menu.Item> + <Menu.ItemLabel>Label</Menu.ItemLabel> + <Menu.ItemHelpText>Help text</Menu.ItemHelpText> + </Menu.Item> + <Menu.Item> + <Menu.ItemLabel>Label</Menu.ItemLabel> + <Menu.ItemHelpText> + The menu item help text is automatically truncated + when there are more than two lines of text + </Menu.ItemHelpText> + </Menu.Item> + <Menu.Item hideOnClick={ false }> + <Menu.ItemLabel>Label</Menu.ItemLabel> + <Menu.ItemHelpText> + This item doesn&apos;t close the menu on click + </Menu.ItemHelpText> + </Menu.Item> + <Menu.Item disabled>Disabled item</Menu.Item> + <Menu.Separator /> + <Menu.Group> + <Menu.GroupLabel>Group label</Menu.GroupLabel> + <Menu.Item + prefix={ <Icon icon={ customLink } size={ 24 } /> } + > + <Menu.ItemLabel>With prefix</Menu.ItemLabel> + </Menu.Item> + <Menu.Item suffix="⌘S">With suffix</Menu.Item> + <Menu.Item + disabled + prefix={ + <Icon icon={ formatCapitalize } size={ 24 } /> + } + suffix="⌥⌘T" + > + <Menu.ItemLabel> + Disabled with prefix and suffix + </Menu.ItemLabel> + <Menu.ItemHelpText>And help text</Menu.ItemHelpText> + </Menu.Item> + </Menu.Group> + </Menu.Popover> + </> + ), + }, }; -export const WithCheckboxes: StoryFn< typeof Menu > = ( props ) => { - const [ isAChecked, setAChecked ] = useState( false ); - const [ isBChecked, setBChecked ] = useState( true ); - const [ multipleCheckboxesValue, setMultipleCheckboxesValue ] = useState< - string[] - >( [ 'b' ] ); - - const onMultipleCheckboxesCheckedChange: React.ComponentProps< - typeof Menu.CheckboxItem - >[ 'onChange' ] = ( e ) => { - setMultipleCheckboxesValue( ( prevValues ) => { - if ( prevValues.includes( e.target.value ) ) { - return prevValues.filter( ( val ) => val !== e.target.value ); - } - return [ ...prevValues, e.target.value ]; - } ); - }; - - return ( - <Menu { ...props }> - <Menu.Group> - <Menu.GroupLabel> - Single selection, uncontrolled - </Menu.GroupLabel> - <Menu.CheckboxItem - name="checkbox-individual-uncontrolled-a" - value="a" - suffix="⌥⌘T" - > - <Menu.ItemLabel>Checkbox item A</Menu.ItemLabel> - <Menu.ItemHelpText>Initially unchecked</Menu.ItemHelpText> - </Menu.CheckboxItem> - <Menu.CheckboxItem - name="checkbox-individual-uncontrolled-b" - value="b" - defaultChecked - > - <Menu.ItemLabel>Checkbox item B</Menu.ItemLabel> - <Menu.ItemHelpText>Initially checked</Menu.ItemHelpText> - </Menu.CheckboxItem> - </Menu.Group> - <Menu.Separator /> - <Menu.Group> - <Menu.GroupLabel>Single selection, controlled</Menu.GroupLabel> - <Menu.CheckboxItem - name="checkbox-individual-controlled-a" - value="a" - checked={ isAChecked } - onChange={ ( e ) => setAChecked( e.target.checked ) } - > - <Menu.ItemLabel>Checkbox item A</Menu.ItemLabel> - <Menu.ItemHelpText>Initially unchecked</Menu.ItemHelpText> - </Menu.CheckboxItem> - <Menu.CheckboxItem - name="checkbox-individual-controlled-b" - value="b" - checked={ isBChecked } - onChange={ ( e ) => setBChecked( e.target.checked ) } - > - <Menu.ItemLabel>Checkbox item B</Menu.ItemLabel> - <Menu.ItemHelpText>Initially checked</Menu.ItemHelpText> - </Menu.CheckboxItem> - </Menu.Group> - <Menu.Separator /> - <Menu.Group> - <Menu.GroupLabel> - Multiple selection, uncontrolled - </Menu.GroupLabel> - <Menu.CheckboxItem - name="checkbox-multiple-uncontrolled" - value="a" - > - <Menu.ItemLabel>Checkbox item A</Menu.ItemLabel> - <Menu.ItemHelpText>Initially unchecked</Menu.ItemHelpText> - </Menu.CheckboxItem> - <Menu.CheckboxItem - name="checkbox-multiple-uncontrolled" - value="b" - defaultChecked - > - <Menu.ItemLabel>Checkbox item B</Menu.ItemLabel> - <Menu.ItemHelpText>Initially checked</Menu.ItemHelpText> - </Menu.CheckboxItem> - </Menu.Group> - <Menu.Separator /> - <Menu.Group> - <Menu.GroupLabel> - Multiple selection, controlled - </Menu.GroupLabel> - <Menu.CheckboxItem - name="checkbox-multiple-controlled" - value="a" - checked={ multipleCheckboxesValue.includes( 'a' ) } - onChange={ onMultipleCheckboxesCheckedChange } - > - <Menu.ItemLabel>Checkbox item A</Menu.ItemLabel> - <Menu.ItemHelpText>Initially unchecked</Menu.ItemHelpText> - </Menu.CheckboxItem> - <Menu.CheckboxItem - name="checkbox-multiple-controlled" - value="b" - checked={ multipleCheckboxesValue.includes( 'b' ) } - onChange={ onMultipleCheckboxesCheckedChange } +export const WithSubmenu: StoryObj< typeof Menu > = { + args: { + ...Default.args, + children: ( + <> + <Menu.TriggerButton + render={ + <Button __next40pxDefaultSize variant="secondary" /> + } > - <Menu.ItemLabel>Checkbox item B</Menu.ItemLabel> - <Menu.ItemHelpText>Initially checked</Menu.ItemHelpText> - </Menu.CheckboxItem> - </Menu.Group> - </Menu> - ); -}; -WithCheckboxes.args = { - ...Default.args, + Open menu + </Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Level 1 item</Menu.Item> + <Menu> + <Menu.SubmenuTriggerItem suffix="Suffix"> + <Menu.ItemLabel> + Submenu trigger item with a long label + </Menu.ItemLabel> + </Menu.SubmenuTriggerItem> + <Menu.Popover> + <Menu.Item> + <Menu.ItemLabel>Level 2 item</Menu.ItemLabel> + </Menu.Item> + <Menu.Item> + <Menu.ItemLabel>Level 2 item</Menu.ItemLabel> + </Menu.Item> + <Menu> + <Menu.SubmenuTriggerItem> + <Menu.ItemLabel> + Submenu trigger + </Menu.ItemLabel> + </Menu.SubmenuTriggerItem> + <Menu.Popover> + <Menu.Item> + <Menu.ItemLabel> + Level 3 item + </Menu.ItemLabel> + </Menu.Item> + <Menu.Item> + <Menu.ItemLabel> + Level 3 item + </Menu.ItemLabel> + </Menu.Item> + </Menu.Popover> + </Menu> + </Menu.Popover> + </Menu> + </Menu.Popover> + </> + ), + }, }; -export const WithRadios: StoryFn< typeof Menu > = ( props ) => { - const [ radioValue, setRadioValue ] = useState( 'two' ); - const onRadioChange: React.ComponentProps< - typeof Menu.RadioItem - >[ 'onChange' ] = ( e ) => setRadioValue( e.target.value ); +export const WithCheckboxes: StoryObj< typeof Menu > = { + render: function WithCheckboxes( props: MenuProps ) { + const [ isAChecked, setAChecked ] = useState( false ); + const [ isBChecked, setBChecked ] = useState( true ); + const [ multipleCheckboxesValue, setMultipleCheckboxesValue ] = + useState< string[] >( [ 'b' ] ); - return ( - <Menu { ...props }> - <Menu.Group> - <Menu.GroupLabel>Uncontrolled</Menu.GroupLabel> - <Menu.RadioItem name="radio-uncontrolled" value="one"> - <Menu.ItemLabel>Radio item 1</Menu.ItemLabel> - <Menu.ItemHelpText>Initially unchecked</Menu.ItemHelpText> - </Menu.RadioItem> - <Menu.RadioItem - name="radio-uncontrolled" - value="two" - defaultChecked - > - <Menu.ItemLabel>Radio item 2</Menu.ItemLabel> - <Menu.ItemHelpText>Initially checked</Menu.ItemHelpText> - </Menu.RadioItem> - </Menu.Group> - <Menu.Separator /> - <Menu.Group> - <Menu.GroupLabel>Controlled</Menu.GroupLabel> - <Menu.RadioItem - name="radio-controlled" - value="one" - checked={ radioValue === 'one' } - onChange={ onRadioChange } - > - <Menu.ItemLabel>Radio item 1</Menu.ItemLabel> - <Menu.ItemHelpText>Initially unchecked</Menu.ItemHelpText> - </Menu.RadioItem> - <Menu.RadioItem - name="radio-controlled" - value="two" - checked={ radioValue === 'two' } - onChange={ onRadioChange } + const onMultipleCheckboxesCheckedChange: React.ComponentProps< + typeof Menu.CheckboxItem + >[ 'onChange' ] = ( e ) => { + setMultipleCheckboxesValue( ( prevValues ) => { + if ( prevValues.includes( e.target.value ) ) { + return prevValues.filter( + ( val ) => val !== e.target.value + ); + } + return [ ...prevValues, e.target.value ]; + } ); + }; + + return ( + <Menu { ...props }> + <Menu.TriggerButton + render={ + <Button __next40pxDefaultSize variant="secondary" /> + } > - <Menu.ItemLabel>Radio item 2</Menu.ItemLabel> - <Menu.ItemHelpText>Initially checked</Menu.ItemHelpText> - </Menu.RadioItem> - </Menu.Group> - </Menu> - ); + Open menu + </Menu.TriggerButton> + <Menu.Popover> + <Menu.Group> + <Menu.GroupLabel> + Single selection, uncontrolled + </Menu.GroupLabel> + <Menu.CheckboxItem + name="checkbox-individual-uncontrolled-a" + value="a" + suffix="⌥⌘T" + > + <Menu.ItemLabel>Checkbox item A</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially unchecked + </Menu.ItemHelpText> + </Menu.CheckboxItem> + <Menu.CheckboxItem + name="checkbox-individual-uncontrolled-b" + value="b" + defaultChecked + > + <Menu.ItemLabel>Checkbox item B</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially checked + </Menu.ItemHelpText> + </Menu.CheckboxItem> + </Menu.Group> + <Menu.Separator /> + <Menu.Group> + <Menu.GroupLabel> + Single selection, controlled + </Menu.GroupLabel> + <Menu.CheckboxItem + name="checkbox-individual-controlled-a" + value="a" + checked={ isAChecked } + onChange={ ( e ) => { + setAChecked( e.target.checked ); + } } + > + <Menu.ItemLabel>Checkbox item A</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially unchecked + </Menu.ItemHelpText> + </Menu.CheckboxItem> + <Menu.CheckboxItem + name="checkbox-individual-controlled-b" + value="b" + checked={ isBChecked } + onChange={ ( e ) => + setBChecked( e.target.checked ) + } + > + <Menu.ItemLabel>Checkbox item B</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially checked + </Menu.ItemHelpText> + </Menu.CheckboxItem> + </Menu.Group> + <Menu.Separator /> + <Menu.Group> + <Menu.GroupLabel> + Multiple selection, uncontrolled + </Menu.GroupLabel> + <Menu.CheckboxItem + name="checkbox-multiple-uncontrolled" + value="a" + > + <Menu.ItemLabel>Checkbox item A</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially unchecked + </Menu.ItemHelpText> + </Menu.CheckboxItem> + <Menu.CheckboxItem + name="checkbox-multiple-uncontrolled" + value="b" + defaultChecked + > + <Menu.ItemLabel>Checkbox item B</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially checked + </Menu.ItemHelpText> + </Menu.CheckboxItem> + </Menu.Group> + <Menu.Separator /> + <Menu.Group> + <Menu.GroupLabel> + Multiple selection, controlled + </Menu.GroupLabel> + <Menu.CheckboxItem + name="checkbox-multiple-controlled" + value="a" + checked={ multipleCheckboxesValue.includes( 'a' ) } + onChange={ onMultipleCheckboxesCheckedChange } + > + <Menu.ItemLabel>Checkbox item A</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially unchecked + </Menu.ItemHelpText> + </Menu.CheckboxItem> + <Menu.CheckboxItem + name="checkbox-multiple-controlled" + value="b" + checked={ multipleCheckboxesValue.includes( 'b' ) } + onChange={ onMultipleCheckboxesCheckedChange } + > + <Menu.ItemLabel>Checkbox item B</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially checked + </Menu.ItemHelpText> + </Menu.CheckboxItem> + </Menu.Group> + </Menu.Popover> + </Menu> + ); + }, + + args: { + ...Default.args, + }, }; -WithRadios.args = { - ...Default.args, + +export const WithRadios: StoryObj< typeof Menu > = { + render: function WithRadios( props: MenuProps ) { + const [ radioValue, setRadioValue ] = useState( 'two' ); + const onRadioChange: React.ComponentProps< + typeof Menu.RadioItem + >[ 'onChange' ] = ( e ) => setRadioValue( e.target.value ); + + return ( + <Menu { ...props }> + <Menu.TriggerButton + render={ + <Button __next40pxDefaultSize variant="secondary" /> + } + > + Open menu + </Menu.TriggerButton> + <Menu.Popover> + <Menu.Group> + <Menu.GroupLabel>Uncontrolled</Menu.GroupLabel> + <Menu.RadioItem name="radio-uncontrolled" value="one"> + <Menu.ItemLabel>Radio item 1</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially unchecked + </Menu.ItemHelpText> + </Menu.RadioItem> + <Menu.RadioItem + name="radio-uncontrolled" + value="two" + defaultChecked + > + <Menu.ItemLabel>Radio item 2</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially checked + </Menu.ItemHelpText> + </Menu.RadioItem> + </Menu.Group> + <Menu.Separator /> + <Menu.Group> + <Menu.GroupLabel>Controlled</Menu.GroupLabel> + <Menu.RadioItem + name="radio-controlled" + value="one" + checked={ radioValue === 'one' } + onChange={ onRadioChange } + > + <Menu.ItemLabel>Radio item 1</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially unchecked + </Menu.ItemHelpText> + </Menu.RadioItem> + <Menu.RadioItem + name="radio-controlled" + value="two" + checked={ radioValue === 'two' } + onChange={ onRadioChange } + > + <Menu.ItemLabel>Radio item 2</Menu.ItemLabel> + <Menu.ItemHelpText> + Initially checked + </Menu.ItemHelpText> + </Menu.RadioItem> + </Menu.Group> + </Menu.Popover> + </Menu> + ); + }, + + args: { + ...Default.args, + }, }; const modalOnTopOfMenuPopover = css` @@ -322,57 +410,72 @@ const modalOnTopOfMenuPopover = css` } `; -// For more examples with `Modal`, check https://ariakit.org/examples/menu-wordpress-modal -export const WithModals: StoryFn< typeof Menu > = ( props ) => { - const [ isOuterModalOpen, setOuterModalOpen ] = useState( false ); - const [ isInnerModalOpen, setInnerModalOpen ] = useState( false ); +export const WithModals: StoryObj< typeof Menu > = { + render: function WithModals( props: MenuProps ) { + const [ isOuterModalOpen, setOuterModalOpen ] = useState( false ); + const [ isInnerModalOpen, setInnerModalOpen ] = useState( false ); - const cx = useCx(); - const modalOverlayClassName = cx( modalOnTopOfMenuPopover ); + const cx = useCx(); + const modalOverlayClassName = cx( modalOnTopOfMenuPopover ); - return ( - <> - <Menu { ...props }> - <Menu.Item - onClick={ () => setOuterModalOpen( true ) } - hideOnClick={ false } - > - <Menu.ItemLabel>Open outer modal</Menu.ItemLabel> - </Menu.Item> - <Menu.Item - onClick={ () => setInnerModalOpen( true ) } - hideOnClick={ false } - > - <Menu.ItemLabel>Open inner modal</Menu.ItemLabel> - </Menu.Item> - { isInnerModalOpen && ( + return ( + <> + <Menu { ...props }> + <Menu.TriggerButton + render={ + <Button __next40pxDefaultSize variant="secondary" /> + } + > + Open menu + </Menu.TriggerButton> + <Menu.Popover> + <Menu.Item + onClick={ () => setOuterModalOpen( true ) } + hideOnClick={ false } + > + <Menu.ItemLabel>Open outer modal</Menu.ItemLabel> + </Menu.Item> + <Menu.Item + onClick={ () => setInnerModalOpen( true ) } + hideOnClick={ false } + > + <Menu.ItemLabel>Open inner modal</Menu.ItemLabel> + </Menu.Item> + { isInnerModalOpen && ( + <Modal + onRequestClose={ () => + setInnerModalOpen( false ) + } + overlayClassName={ modalOverlayClassName } + > + Modal&apos;s contents + <button + onClick={ () => setInnerModalOpen( false ) } + > + Close + </button> + </Modal> + ) } + </Menu.Popover> + </Menu> + { isOuterModalOpen && ( <Modal - onRequestClose={ () => setInnerModalOpen( false ) } + onRequestClose={ () => setOuterModalOpen( false ) } overlayClassName={ modalOverlayClassName } > Modal&apos;s contents - <button onClick={ () => setInnerModalOpen( false ) }> + <button onClick={ () => setOuterModalOpen( false ) }> Close </button> </Modal> ) } - </Menu> - { isOuterModalOpen && ( - <Modal - onRequestClose={ () => setOuterModalOpen( false ) } - overlayClassName={ modalOverlayClassName } - > - Modal&apos;s contents - <button onClick={ () => setOuterModalOpen( false ) }> - Close - </button> - </Modal> - ) } - </> - ); -}; -WithModals.args = { - ...Default.args, + </> + ); + }, + + args: { + ...Default.args, + }, }; const ExampleSlotFill = createSlotFill( 'Example' ); @@ -423,37 +526,50 @@ const Fill = ( { children }: { children: React.ReactNode } ) => { ); }; -export const WithSlotFill: StoryFn< typeof Menu > = ( props ) => { - return ( - <SlotFillProvider> - <Menu { ...props }> - <Menu.Item> - <Menu.ItemLabel>Item</Menu.ItemLabel> - </Menu.Item> - <Slot /> - </Menu> - - <Fill> - <Menu.Item> - <Menu.ItemLabel>Item from fill</Menu.ItemLabel> - </Menu.Item> - <Menu - trigger={ +export const WithSlotFill: StoryObj< typeof Menu > = { + render: ( props: MenuProps ) => { + return ( + <SlotFillProvider> + <Menu { ...props }> + <Menu.TriggerButton + render={ + <Button __next40pxDefaultSize variant="secondary" /> + } + > + Open menu + </Menu.TriggerButton> + <Menu.Popover> <Menu.Item> - <Menu.ItemLabel>Submenu from fill</Menu.ItemLabel> + <Menu.ItemLabel>Item</Menu.ItemLabel> </Menu.Item> - } - > + <Slot /> + </Menu.Popover> + </Menu> + + <Fill> <Menu.Item> - <Menu.ItemLabel>Submenu item from fill</Menu.ItemLabel> + <Menu.ItemLabel>Item from fill</Menu.ItemLabel> </Menu.Item> - </Menu> - </Fill> - </SlotFillProvider> - ); -}; -WithSlotFill.args = { - ...Default.args, + <Menu> + <Menu.SubmenuTriggerItem> + <Menu.ItemLabel>Submenu from fill</Menu.ItemLabel> + </Menu.SubmenuTriggerItem> + <Menu.Popover> + <Menu.Item> + <Menu.ItemLabel> + Submenu item from fill + </Menu.ItemLabel> + </Menu.Item> + </Menu.Popover> + </Menu> + </Fill> + </SlotFillProvider> + ); + }, + + args: { + ...Default.args, + }, }; const toolbarVariantContextValue = { @@ -461,83 +577,119 @@ const toolbarVariantContextValue = { variant: 'toolbar', }, }; -export const ToolbarVariant: StoryFn< typeof Menu > = ( props ) => ( - // TODO: add toolbar - <ContextSystemProvider value={ toolbarVariantContextValue }> - <Menu { ...props }> - <Menu.Item> - <Menu.ItemLabel>Level 1 item</Menu.ItemLabel> - </Menu.Item> - <Menu.Item> - <Menu.ItemLabel>Level 1 item</Menu.ItemLabel> - </Menu.Item> - <Menu.Separator /> - <Menu - trigger={ + +export const ToolbarVariant: StoryObj< typeof Menu > = { + render: ( props: MenuProps ) => ( + // TODO: add toolbar + <ContextSystemProvider value={ toolbarVariantContextValue }> + <Menu { ...props }> + <Menu.TriggerButton + render={ + <Button __next40pxDefaultSize variant="secondary" /> + } + > + Open menu + </Menu.TriggerButton> + <Menu.Popover> <Menu.Item> - <Menu.ItemLabel>Submenu trigger</Menu.ItemLabel> + <Menu.ItemLabel>Level 1 item</Menu.ItemLabel> </Menu.Item> - } - > - <Menu.Item> - <Menu.ItemLabel>Level 2 item</Menu.ItemLabel> - </Menu.Item> + <Menu.Item> + <Menu.ItemLabel>Level 1 item</Menu.ItemLabel> + </Menu.Item> + <Menu.Separator /> + <Menu> + <Menu.SubmenuTriggerItem> + <Menu.ItemLabel>Submenu trigger</Menu.ItemLabel> + </Menu.SubmenuTriggerItem> + <Menu.Popover> + <Menu.Item> + <Menu.ItemLabel>Level 2 item</Menu.ItemLabel> + </Menu.Item> + </Menu.Popover> + </Menu> + </Menu.Popover> </Menu> - </Menu> - </ContextSystemProvider> -); -ToolbarVariant.args = { - ...Default.args, + </ContextSystemProvider> + ), + + args: { + ...Default.args, + }, }; -export const InsideModal: StoryFn< typeof Menu > = ( props ) => { - const [ isModalOpen, setModalOpen ] = useState( false ); - return ( - <> - <Button - onClick={ () => setModalOpen( true ) } - __next40pxDefaultSize - variant="secondary" - > - Open modal - </Button> - { isModalOpen && ( - <Modal onRequestClose={ () => setModalOpen( false ) }> - <Menu { ...props }> - <Menu.Item> - <Menu.ItemLabel>Level 1 item</Menu.ItemLabel> - </Menu.Item> - <Menu.Item> - <Menu.ItemLabel>Level 1 item</Menu.ItemLabel> - </Menu.Item> - <Menu.Separator /> - <Menu - trigger={ +export const InsideModal: StoryObj< typeof Menu > = { + render: function InsideModal( props: MenuProps ) { + const [ isModalOpen, setModalOpen ] = useState( false ); + return ( + <> + <Button + onClick={ () => setModalOpen( true ) } + __next40pxDefaultSize + variant="secondary" + > + Open modal + </Button> + { isModalOpen && ( + <Modal + onRequestClose={ () => setModalOpen( false ) } + title="Menu inside modal" + > + <Menu { ...props }> + <Menu.TriggerButton + render={ + <Button + __next40pxDefaultSize + variant="secondary" + /> + } + > + Open menu + </Menu.TriggerButton> + <Menu.Popover> <Menu.Item> <Menu.ItemLabel> - Submenu trigger + Level 1 item </Menu.ItemLabel> </Menu.Item> - } - > - <Menu.Item> - <Menu.ItemLabel>Level 2 item</Menu.ItemLabel> - </Menu.Item> + <Menu.Item> + <Menu.ItemLabel> + Level 1 item + </Menu.ItemLabel> + </Menu.Item> + <Menu.Separator /> + <Menu> + <Menu.SubmenuTriggerItem> + <Menu.ItemLabel> + Submenu trigger + </Menu.ItemLabel> + </Menu.SubmenuTriggerItem> + <Menu.Popover> + <Menu.Item> + <Menu.ItemLabel> + Level 2 item + </Menu.ItemLabel> + </Menu.Item> + </Menu.Popover> + </Menu> + </Menu.Popover> </Menu> - </Menu> - <Button onClick={ () => setModalOpen( false ) }> - Close modal - </Button> - </Modal> - ) } - </> - ); -}; -InsideModal.args = { - ...Default.args, -}; -InsideModal.parameters = { - docs: { - source: { type: 'code' }, + <Button onClick={ () => setModalOpen( false ) }> + Close modal + </Button> + </Modal> + ) } + </> + ); + }, + + args: { + ...Default.args, + }, + + parameters: { + docs: { + source: { type: 'code' }, + }, }, }; diff --git a/packages/components/src/menu/submenu-trigger-item.tsx b/packages/components/src/menu/submenu-trigger-item.tsx new file mode 100644 index 00000000000000..23932a14bdaff4 --- /dev/null +++ b/packages/components/src/menu/submenu-trigger-item.tsx @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; +import { chevronRightSmall } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import type { MenuItemProps } from './types'; +import { MenuContext } from './context'; +import { MenuItem } from './item'; +import * as Styled from './styles'; + +export const MenuSubmenuTriggerItem = forwardRef< + HTMLDivElement, + WordPressComponentProps< MenuItemProps, 'div', false > +>( function MenuSubmenuTriggerItem( { suffix, ...otherProps }, ref ) { + const menuContext = useContext( MenuContext ); + + if ( ! menuContext?.store.parent ) { + throw new Error( + 'Menu.SubmenuTriggerItem can only be rendered inside a nested Menu component' + ); + } + + return ( + <Ariakit.MenuButton + ref={ ref } + accessibleWhenDisabled + store={ menuContext.store } + render={ + <MenuItem + { ...otherProps } + // The menu item needs to register and be part of the parent menu. + // Without specifying the store explicitly, the `MenuItem` component + // would otherwise read the store via context and pick up the one from + // the sub-menu `Menu` component. + store={ menuContext.store.parent } + suffix={ + <> + { suffix } + <Styled.SubmenuChevronIcon + aria-hidden="true" + icon={ chevronRightSmall } + size={ 24 } + preserveAspectRatio="xMidYMid slice" + /> + </> + } + /> + } + /> + ); +} ); diff --git a/packages/components/src/menu/test/index.tsx b/packages/components/src/menu/test/index.tsx index 60276cdb2379a0..42e1516d94bbba 100644 --- a/packages/components/src/menu/test/index.tsx +++ b/packages/components/src/menu/test/index.tsx @@ -18,17 +18,28 @@ const delay = ( delayInMs: number ) => { return new Promise( ( resolve ) => setTimeout( resolve, delayInMs ) ); }; +// Open dropdown => open menu +// Submenu trigger item => open submenu + describe( 'Menu', () => { // See https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/ it( 'should follow the WAI-ARIA spec', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item>Menu item</Menu.Item> - <Menu.Separator /> - <Menu trigger={ <Menu.Item>Submenu trigger item</Menu.Item> }> - <Menu.Item>Submenu item 1</Menu.Item> - <Menu.Item>Submenu item 2</Menu.Item> - </Menu> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Menu item</Menu.Item> + <Menu.Separator /> + <Menu> + <Menu.SubmenuTriggerItem> + Submenu trigger item + </Menu.SubmenuTriggerItem> + <Menu.Popover> + <Menu.Item>Submenu item 1</Menu.Item> + <Menu.Item>Submenu item 2</Menu.Item> + </Menu.Popover> + </Menu> + </Menu.Popover> </Menu> ); @@ -84,8 +95,11 @@ describe( 'Menu', () => { describe( 'pointer and keyboard interactions', () => { it( 'should open and focus the menu when clicking the trigger', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item>Menu item</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Menu item</Menu.Item> + </Menu.Popover> </Menu> ); @@ -105,10 +119,13 @@ describe( 'Menu', () => { it( 'should open and focus the first item when pressing the arrow down key on the trigger', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item disabled>First item</Menu.Item> - <Menu.Item>Second item</Menu.Item> - <Menu.Item>Third item</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item disabled>First item</Menu.Item> + <Menu.Item>Second item</Menu.Item> + <Menu.Item>Third item</Menu.Item> + </Menu.Popover> </Menu> ); @@ -135,10 +152,13 @@ describe( 'Menu', () => { it( 'should open and focus the first item when pressing the space key on the trigger', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item disabled>First item</Menu.Item> - <Menu.Item>Second item</Menu.Item> - <Menu.Item>Third item</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item disabled>First item</Menu.Item> + <Menu.Item>Second item</Menu.Item> + <Menu.Item>Third item</Menu.Item> + </Menu.Popover> </Menu> ); @@ -165,8 +185,11 @@ describe( 'Menu', () => { it( 'should close when pressing the escape key', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item>Menu item</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Menu item</Menu.Item> + </Menu.Popover> </Menu> ); @@ -194,8 +217,11 @@ describe( 'Menu', () => { it( 'should close when clicking outside of the content', async () => { render( - <Menu defaultOpen trigger={ <button>Open dropdown</button> }> - <Menu.Item>Menu item</Menu.Item> + <Menu defaultOpen> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Menu item</Menu.Item> + </Menu.Popover> </Menu> ); @@ -209,8 +235,11 @@ describe( 'Menu', () => { it( 'should close when clicking on a menu item', async () => { render( - <Menu defaultOpen trigger={ <button>Open dropdown</button> }> - <Menu.Item>Menu item</Menu.Item> + <Menu defaultOpen> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Menu item</Menu.Item> + </Menu.Popover> </Menu> ); @@ -224,8 +253,11 @@ describe( 'Menu', () => { it( 'should not close when clicking on a menu item when the `hideOnClick` prop is set to `false`', async () => { render( - <Menu defaultOpen trigger={ <button>Open dropdown</button> }> - <Menu.Item hideOnClick={ false }>Menu item</Menu.Item> + <Menu defaultOpen> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item hideOnClick={ false }>Menu item</Menu.Item> + </Menu.Popover> </Menu> ); @@ -239,8 +271,11 @@ describe( 'Menu', () => { it( 'should not close when clicking on a disabled menu item', async () => { render( - <Menu defaultOpen trigger={ <button>Open dropdown</button> }> - <Menu.Item disabled>Menu item</Menu.Item> + <Menu defaultOpen> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item disabled>Menu item</Menu.Item> + </Menu.Popover> </Menu> ); @@ -254,16 +289,22 @@ describe( 'Menu', () => { it( 'should reveal submenu content when hovering over the submenu trigger', async () => { render( - <Menu defaultOpen trigger={ <button>Open dropdown</button> }> - <Menu.Item>Menu item 1</Menu.Item> - <Menu.Item>Menu item 2</Menu.Item> - <Menu - trigger={ <Menu.Item>Submenu trigger item</Menu.Item> } - > - <Menu.Item>Submenu item 1</Menu.Item> - <Menu.Item>Submenu item 2</Menu.Item> - </Menu> - <Menu.Item>Menu item 3</Menu.Item> + <Menu defaultOpen> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Menu item 1</Menu.Item> + <Menu.Item>Menu item 2</Menu.Item> + <Menu> + <Menu.SubmenuTriggerItem> + Submenu trigger item + </Menu.SubmenuTriggerItem> + <Menu.Popover> + <Menu.Item>Submenu item 1</Menu.Item> + <Menu.Item>Submenu item 2</Menu.Item> + </Menu.Popover> + </Menu> + <Menu.Item>Menu item 3</Menu.Item> + </Menu.Popover> </Menu> ); @@ -288,16 +329,22 @@ describe( 'Menu', () => { it( 'should navigate menu items and subitems using the arrow, spacebar and enter keys', async () => { render( - <Menu defaultOpen trigger={ <button>Open dropdown</button> }> - <Menu.Item>Menu item 1</Menu.Item> - <Menu.Item>Menu item 2</Menu.Item> - <Menu - trigger={ <Menu.Item>Submenu trigger item</Menu.Item> } - > - <Menu.Item>Submenu item 1</Menu.Item> - <Menu.Item>Submenu item 2</Menu.Item> - </Menu> - <Menu.Item>Menu item 3</Menu.Item> + <Menu defaultOpen> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Menu item 1</Menu.Item> + <Menu.Item>Menu item 2</Menu.Item> + <Menu> + <Menu.SubmenuTriggerItem> + Submenu trigger item + </Menu.SubmenuTriggerItem> + <Menu.Popover> + <Menu.Item>Submenu item 1</Menu.Item> + <Menu.Item>Submenu item 2</Menu.Item> + </Menu.Popover> + </Menu> + <Menu.Item>Menu item 3</Menu.Item> + </Menu.Popover> </Menu> ); @@ -407,25 +454,28 @@ describe( 'Menu', () => { setRadioValue( e.target.value ); }; return ( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Group> - <Menu.RadioItem - name="radio-test" - value="radio-one" - checked={ radioValue === 'radio-one' } - onChange={ onRadioChange } - > - Radio item one - </Menu.RadioItem> - <Menu.RadioItem - name="radio-test" - value="radio-two" - checked={ radioValue === 'radio-two' } - onChange={ onRadioChange } - > - Radio item two - </Menu.RadioItem> - </Menu.Group> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Group> + <Menu.RadioItem + name="radio-test" + value="radio-one" + checked={ radioValue === 'radio-one' } + onChange={ onRadioChange } + > + Radio item one + </Menu.RadioItem> + <Menu.RadioItem + name="radio-test" + value="radio-two" + checked={ radioValue === 'radio-two' } + onChange={ onRadioChange } + > + Radio item two + </Menu.RadioItem> + </Menu.Group> + </Menu.Popover> </Menu> ); }; @@ -484,28 +534,31 @@ describe( 'Menu', () => { it( 'should check radio items and keep the menu open when clicking (uncontrolled)', async () => { const onRadioValueChangeSpy = jest.fn(); render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Group> - <Menu.RadioItem - name="radio-test" - value="radio-one" - onChange={ ( e ) => - onRadioValueChangeSpy( e.target.value ) - } - > - Radio item one - </Menu.RadioItem> - <Menu.RadioItem - name="radio-test" - value="radio-two" - defaultChecked - onChange={ ( e ) => - onRadioValueChangeSpy( e.target.value ) - } - > - Radio item two - </Menu.RadioItem> - </Menu.Group> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Group> + <Menu.RadioItem + name="radio-test" + value="radio-one" + onChange={ ( e ) => + onRadioValueChangeSpy( e.target.value ) + } + > + Radio item one + </Menu.RadioItem> + <Menu.RadioItem + name="radio-test" + value="radio-two" + defaultChecked + onChange={ ( e ) => + onRadioValueChangeSpy( e.target.value ) + } + > + Radio item two + </Menu.RadioItem> + </Menu.Group> + </Menu.Popover> </Menu> ); @@ -568,38 +621,41 @@ describe( 'Menu', () => { useState< boolean >(); return ( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.CheckboxItem - name="item-one" - value="item-one-value" - checked={ itemOneChecked } - onChange={ ( e ) => { - onCheckboxValueChangeSpy( - e.target.name, - e.target.value, - e.target.checked - ); - setItemOneChecked( e.target.checked ); - } } - > - Checkbox item one - </Menu.CheckboxItem> - - <Menu.CheckboxItem - name="item-two" - value="item-two-value" - checked={ itemTwoChecked } - onChange={ ( e ) => { - onCheckboxValueChangeSpy( - e.target.name, - e.target.value, - e.target.checked - ); - setItemTwoChecked( e.target.checked ); - } } - > - Checkbox item two - </Menu.CheckboxItem> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.CheckboxItem + name="item-one" + value="item-one-value" + checked={ itemOneChecked } + onChange={ ( e ) => { + onCheckboxValueChangeSpy( + e.target.name, + e.target.value, + e.target.checked + ); + setItemOneChecked( e.target.checked ); + } } + > + Checkbox item one + </Menu.CheckboxItem> + + <Menu.CheckboxItem + name="item-two" + value="item-two-value" + checked={ itemTwoChecked } + onChange={ ( e ) => { + onCheckboxValueChangeSpy( + e.target.name, + e.target.value, + e.target.checked + ); + setItemTwoChecked( e.target.checked ); + } } + > + Checkbox item two + </Menu.CheckboxItem> + </Menu.Popover> </Menu> ); }; @@ -691,35 +747,38 @@ describe( 'Menu', () => { const onCheckboxValueChangeSpy = jest.fn(); render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.CheckboxItem - name="item-one" - value="item-one-value" - onChange={ ( e ) => { - onCheckboxValueChangeSpy( - e.target.name, - e.target.value, - e.target.checked - ); - } } - > - Checkbox item one - </Menu.CheckboxItem> - - <Menu.CheckboxItem - name="item-two" - value="item-two-value" - defaultChecked - onChange={ ( e ) => { - onCheckboxValueChangeSpy( - e.target.name, - e.target.value, - e.target.checked - ); - } } - > - Checkbox item two - </Menu.CheckboxItem> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.CheckboxItem + name="item-one" + value="item-one-value" + onChange={ ( e ) => { + onCheckboxValueChangeSpy( + e.target.name, + e.target.value, + e.target.checked + ); + } } + > + Checkbox item one + </Menu.CheckboxItem> + + <Menu.CheckboxItem + name="item-two" + value="item-two-value" + defaultChecked + onChange={ ( e ) => { + onCheckboxValueChangeSpy( + e.target.name, + e.target.value, + e.target.checked + ); + } } + > + Checkbox item two + </Menu.CheckboxItem> + </Menu.Popover> </Menu> ); @@ -809,8 +868,11 @@ describe( 'Menu', () => { it( 'should be modal by default', async () => { render( <> - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item>Menu item</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>Menu item</Menu.Item> + </Menu.Popover> </Menu> <button>Button outside of dropdown</button> </> @@ -836,11 +898,11 @@ describe( 'Menu', () => { it( 'should not be modal when the `modal` prop is set to `false`', async () => { render( <> - <Menu - trigger={ <button>Open dropdown</button> } - modal={ false } - > - <Menu.Item>Menu item</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover modal={ false }> + <Menu.Item>Menu item</Menu.Item> + </Menu.Popover> </Menu> <button>Button outside of dropdown</button> </> @@ -873,8 +935,13 @@ describe( 'Menu', () => { describe( 'items prefix and suffix', () => { it( 'should display a prefix on regular items', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item prefix={ <>Item prefix</> }>Menu item</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item prefix={ <>Item prefix</> }> + Menu item + </Menu.Item> + </Menu.Popover> </Menu> ); @@ -895,8 +962,13 @@ describe( 'Menu', () => { it( 'should display a suffix on regular items', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item suffix={ <>Item suffix</> }>Menu item</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item suffix={ <>Item suffix</> }> + Menu item + </Menu.Item> + </Menu.Popover> </Menu> ); @@ -917,14 +989,17 @@ describe( 'Menu', () => { it( 'should display a suffix on radio items', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.RadioItem - name="radio-test" - value="radio-one" - suffix="Radio suffix" - > - Radio item one - </Menu.RadioItem> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.RadioItem + name="radio-test" + value="radio-one" + suffix="Radio suffix" + > + Radio item one + </Menu.RadioItem> + </Menu.Popover> </Menu> ); @@ -945,14 +1020,17 @@ describe( 'Menu', () => { it( 'should display a suffix on checkbox items', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.CheckboxItem - name="checkbox-test" - value="checkbox-one" - suffix="Checkbox suffix" - > - Checkbox item one - </Menu.CheckboxItem> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.CheckboxItem + name="checkbox-test" + value="checkbox-one" + suffix="Checkbox suffix" + > + Checkbox item one + </Menu.CheckboxItem> + </Menu.Popover> </Menu> ); @@ -975,9 +1053,12 @@ describe( 'Menu', () => { describe( 'typeahead', () => { it( 'should highlight matching item', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item>One</Menu.Item> - <Menu.Item>Two</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>One</Menu.Item> + <Menu.Item>Two</Menu.Item> + </Menu.Popover> </Menu> ); @@ -1008,9 +1089,12 @@ describe( 'Menu', () => { it( 'should keep previous focus when no matches are found', async () => { render( - <Menu trigger={ <button>Open dropdown</button> }> - <Menu.Item>One</Menu.Item> - <Menu.Item>Two</Menu.Item> + <Menu> + <Menu.TriggerButton>Open dropdown</Menu.TriggerButton> + <Menu.Popover> + <Menu.Item>One</Menu.Item> + <Menu.Item>Two</Menu.Item> + </Menu.Popover> </Menu> ); diff --git a/packages/components/src/menu/trigger-button.tsx b/packages/components/src/menu/trigger-button.tsx new file mode 100644 index 00000000000000..b99804efef0f17 --- /dev/null +++ b/packages/components/src/menu/trigger-button.tsx @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import type { MenuTriggerButtonProps } from './types'; +import { MenuContext } from './context'; + +export const MenuTriggerButton = forwardRef< + HTMLDivElement, + WordPressComponentProps< MenuTriggerButtonProps, 'button', false > +>( function MenuTriggerButton( { children, disabled = false, ...props }, ref ) { + const menuContext = useContext( MenuContext ); + + if ( ! menuContext?.store ) { + throw new Error( + 'Menu.TriggerButton can only be rendered inside a Menu component' + ); + } + + if ( menuContext.store.parent ) { + throw new Error( + 'Menu.TriggerButton should not be rendered inside a nested Menu component. Use Menu.SubmenuTriggerItem instead.' + ); + } + + return ( + <Ariakit.MenuButton + ref={ ref } + { ...props } + disabled={ disabled } + store={ menuContext.store } + > + { children } + </Ariakit.MenuButton> + ); +} ); diff --git a/packages/components/src/menu/types.ts b/packages/components/src/menu/types.ts index 7b58cef241743e..f9bb0782529d1f 100644 --- a/packages/components/src/menu/types.ts +++ b/packages/components/src/menu/types.ts @@ -2,7 +2,6 @@ * External dependencies */ import type * as Ariakit from '@ariakit/react'; -import type { Placement } from '@floating-ui/react-dom'; export interface MenuContext { /** @@ -17,170 +16,318 @@ export interface MenuContext { export interface MenuProps { /** - * The button triggering the menu popover. + * The elements, which should include one instance of the `Menu.TriggerButton` + * component and one instance of the `Menu.Popover` component. */ - trigger: React.ReactElement; + children?: Ariakit.MenuProviderProps[ 'children' ]; /** - * The contents of the menu (ie. one or more menu items). + * Whether the menu popover and its contents should be visible by default. + * + * Note: this prop will be overridden by the `open` prop if it is + * provided (meaning the component will be used in "controlled" mode). + * + * @default false */ - children?: React.ReactNode; + defaultOpen?: Ariakit.MenuProviderProps[ 'defaultOpen' ]; /** - * The open state of the menu popover when it is initially rendered. Use when - * not wanting to control its open state. + * Whether the menu popover and its contents should be visible. + * Should be used in conjunction with `onOpenChange` in order to control + * the open state of the menu popover. * - * @default false + * Note: this prop will set the component in "controlled" mode, and it will + * override the `defaultOpen` prop. */ - defaultOpen?: boolean; + open?: Ariakit.MenuProviderProps[ 'open' ]; /** - * The controlled open state of the menu popover. Must be used in conjunction - * with `onOpenChange`. + * A callback that gets called when the `open` state changes. */ - open?: boolean; + onOpenChange?: Ariakit.MenuProviderProps[ 'setOpen' ]; /** - * Event handler called when the open state of the menu popover changes. + * The placement of the menu popover. + * + * @default 'bottom-start' for root-level menus, 'right-start' for submenus */ - onOpenChange?: ( open: boolean ) => void; + placement?: Ariakit.MenuProviderProps[ 'placement' ]; +} + +export interface MenuPopoverProps { + /** + * The contents of the menu popover, which should include instances of the + * `Menu.Item`, `Menu.CheckboxItem`, `Menu.RadioItem`, `Menu.Group`, and + * `Menu.Separator` components. + */ + children?: Ariakit.MenuProps[ 'children' ]; /** * The modality of the menu popover. When set to true, interaction with * outside elements will be disabled and only menu content will be visible to * screen readers. * - * @default true - */ - modal?: boolean; - /** - * The placement of the menu popover. + * Determines whether the menu popover is modal. Modal dialogs have distinct + * states and behaviors: + * - The `portal` and `preventBodyScroll` props are set to `true`. They can + * still be manually set to `false`. + * - When the dialog is open, element tree outside it will be inert. * - * @default 'bottom-start' for root-level menus, 'right-start' for nested menus + * @default true */ - placement?: Placement; + modal?: Ariakit.MenuProps[ 'modal' ]; /** * The distance between the popover and the anchor element. * * @default 8 for root-level menus, 16 for nested menus */ - gutter?: number; + gutter?: Ariakit.MenuProps[ 'gutter' ]; /** * The skidding of the popover along the anchor element. Can be set to * negative values to make the popover shift to the opposite side. * * @default 0 for root-level menus, -8 for nested menus */ - shift?: number; + shift?: Ariakit.MenuProps[ 'shift' ]; /** - * Determines whether the menu popover will be hidden when the user presses - * the Escape key. + * Determines if the menu popover will hide when the user presses the + * Escape key. + * + * This prop can be either a boolean or a function that accepts an event as an + * argument and returns a boolean. The event object represents the keydown + * event that initiated the hide action, which could be either a native + * keyboard event or a React synthetic event. * * @default `( event ) => { event.preventDefault(); return true; }` */ - hideOnEscape?: - | boolean - | ( ( - event: KeyboardEvent | React.KeyboardEvent< Element > - ) => boolean ); + hideOnEscape?: Ariakit.MenuProps[ 'hideOnEscape' ]; +} + +export interface MenuTriggerButtonProps { + /** + * The contents of the menu trigger button. + */ + children?: Ariakit.MenuButtonProps[ 'children' ]; + /** + * Allows the component to be rendered as a different HTML element or React + * component. The value can be a React element or a function that takes in the + * original component props and gives back a React element with the props + * merged. + */ + render?: Ariakit.MenuButtonProps[ 'render' ]; + /** + * Determines if the element is disabled. This sets the `aria-disabled` + * attribute accordingly, enabling support for all elements, including those + * that don't support the native `disabled` attribute. + * + * This feature can be combined with the `accessibleWhenDisabled` prop to + * make disabled elements still accessible via keyboard. + * + * @default false + */ + disabled?: Ariakit.MenuButtonProps[ 'disabled' ]; + /** + * Indicates whether the element should be focusable even when it is + * `disabled`. + * + * This is important when discoverability is a concern. For example: + * + * > A toolbar in an editor contains a set of special smart paste functions + * that are disabled when the clipboard is empty or when the function is not + * applicable to the current content of the clipboard. It could be helpful to + * keep the disabled buttons focusable if the ability to discover their + * functionality is primarily via their presence on the toolbar. + * + * Learn more on [Focusability of disabled + * controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + */ + accessibleWhenDisabled?: Ariakit.MenuButtonProps[ 'accessibleWhenDisabled' ]; } export interface MenuGroupProps { /** - * The contents of the menu group (ie. an optional menu group label and one - * or more menu items). + * The contents of the menu group, which should include one instance of the + * `Menu.GroupLabel` component and one or more instances of `Menu.Item`, + * `Menu.CheckboxItem`, and `Menu.RadioItem`. */ - children: React.ReactNode; + children: Ariakit.MenuGroupProps[ 'children' ]; } export interface MenuGroupLabelProps { /** - * The contents of the menu group label. + * The contents of the menu group label, which should provide an accessible + * label for the menu group. */ - children: React.ReactNode; + children: Ariakit.MenuGroupLabelProps[ 'children' ]; } export interface MenuItemProps { /** - * The contents of the menu item. + * The contents of the menu item, which could include one instance of the + * `Menu.ItemLabel` component and/or one instance of the `Menu.ItemHelpText` + * component. */ - children: React.ReactNode; + children: Ariakit.MenuItemProps[ 'children' ]; /** - * The contents of the menu item's prefix. + * The contents of the menu item's prefix, such as an icon. */ prefix?: React.ReactNode; /** - * The contents of the menu item's suffix. + * The contents of the menu item's suffix, such as a keyboard shortcut. */ suffix?: React.ReactNode; /** - * Whether to hide the menu popover when the menu item is clicked. + * Determines if the menu should hide when this item is clicked. + * + * **Note**: This behavior isn't triggered if this menu item is rendered as a + * link and modifier keys are used to either open the link in a new tab or + * download it. * * @default true */ - hideOnClick?: boolean; + hideOnClick?: Ariakit.MenuItemProps[ 'hideOnClick' ]; /** - * Determines if the element is disabled. + * Determines if the element is disabled. This sets the `aria-disabled` + * attribute accordingly, enabling support for all elements, including those + * that don't support the native `disabled` attribute. + * + * @default false + */ + disabled?: Ariakit.MenuItemProps[ 'disabled' ]; + /** + * Allows the component to be rendered as a different HTML element or React + * component. The value can be a React element or a function that takes in the + * original component props and gives back a React element with the props + * merged. + */ + render?: Ariakit.MenuItemProps[ 'render' ]; + /** + * The ariakit menu store. This prop is only meant for internal use. + * @ignore */ - disabled?: boolean; + store?: Ariakit.MenuItemProps[ 'store' ]; } -export interface MenuCheckboxItemProps - extends Omit< MenuItemProps, 'prefix' | 'hideOnClick' > { +export interface MenuCheckboxItemProps { /** - * Whether to hide the menu popover when the menu item is clicked. + * The contents of the menu item, which could include one instance of the + * `Menu.ItemLabel` component and/or one instance of the `Menu.ItemHelpText` + * component. + */ + children: Ariakit.MenuItemCheckboxProps[ 'children' ]; + /** + * The contents of the menu item's suffix, such as a keyboard shortcut. + */ + suffix?: React.ReactNode; + /** + * Determines if the menu should hide when this item is clicked. + * + * **Note**: This behavior isn't triggered if this menu item is rendered as a + * link and modifier keys are used to either open the link in a new tab or + * download it. + * + * @default false + */ + hideOnClick?: Ariakit.MenuItemCheckboxProps[ 'hideOnClick' ]; + /** + * Determines if the element is disabled. This sets the `aria-disabled` + * attribute accordingly, enabling support for all elements, including those + * that don't support the native `disabled` attribute. * * @default false */ - hideOnClick?: boolean; + disabled?: Ariakit.MenuItemCheckboxProps[ 'disabled' ]; + /** + * Allows the component to be rendered as a different HTML element or React + * component. The value can be a React element or a function that takes in the + * original component props and gives back a React element with the props + * merged. + */ + render?: Ariakit.MenuItemCheckboxProps[ 'render' ]; /** * The checkbox menu item's name. */ - name: string; + name: Ariakit.MenuItemCheckboxProps[ 'name' ]; /** * The checkbox item's value, useful when using multiple checkbox menu items * associated to the same `name`. */ - value?: string; + value?: Ariakit.MenuItemCheckboxProps[ 'value' ]; /** * The controlled checked state of the checkbox menu item. + * + * Note: this prop will override the `defaultChecked` prop. */ - checked?: boolean; + checked?: Ariakit.MenuItemCheckboxProps[ 'checked' ]; /** * The checked state of the checkbox menu item when it is initially rendered. * Use when not wanting to control its checked state. + * + * Note: this prop will be overriden by the `checked` prop, if it is defined. */ - defaultChecked?: boolean; + defaultChecked?: Ariakit.MenuItemCheckboxProps[ 'defaultChecked' ]; /** - * Event handler called when the checked state of the checkbox menu item changes. + * A function that is called when the checkbox's checked state changes. */ - onChange?: ( event: React.ChangeEvent< HTMLInputElement > ) => void; + onChange?: Ariakit.MenuItemCheckboxProps[ 'onChange' ]; } -export interface MenuRadioItemProps - extends Omit< MenuItemProps, 'prefix' | 'hideOnClick' > { +export interface MenuRadioItemProps { + /** + * The contents of the menu item, which could include one instance of the + * `Menu.ItemLabel` component and/or one instance of the `Menu.ItemHelpText` + * component. + */ + children: Ariakit.MenuItemRadioProps[ 'children' ]; + /** + * The contents of the menu item's suffix, such as a keyboard shortcut. + */ + suffix?: React.ReactNode; + /** + * Determines if the menu should hide when this item is clicked. + * + * **Note**: This behavior isn't triggered if this menu item is rendered as a + * link and modifier keys are used to either open the link in a new tab or + * download it. + * + * @default false + */ + hideOnClick?: Ariakit.MenuItemRadioProps[ 'hideOnClick' ]; /** - * Whether to hide the menu popover when the menu item is clicked. + * Determines if the element is disabled. This sets the `aria-disabled` + * attribute accordingly, enabling support for all elements, including those + * that don't support the native `disabled` attribute. * * @default false */ - hideOnClick?: boolean; + disabled?: Ariakit.MenuItemRadioProps[ 'disabled' ]; + /** + * Allows the component to be rendered as a different HTML element or React + * component. The value can be a React element or a function that takes in the + * original component props and gives back a React element with the props + * merged. + */ + render?: Ariakit.MenuItemRadioProps[ 'render' ]; /** * The radio item's name. */ - name: string; + name: Ariakit.MenuItemRadioProps[ 'name' ]; /** * The radio item's value. */ - value: string | number; + value: Ariakit.MenuItemRadioProps[ 'value' ]; /** * The controlled checked state of the radio menu item. + * + * Note: this prop will override the `defaultChecked` prop. */ - checked?: boolean; + checked?: Ariakit.MenuItemRadioProps[ 'checked' ]; /** * The checked state of the radio menu item when it is initially rendered. * Use when not wanting to control its checked state. + * + * Note: this prop will be overriden by the `checked` prop, if it is defined. */ - defaultChecked?: boolean; + defaultChecked?: Ariakit.MenuItemRadioProps[ 'defaultChecked' ]; /** - * Event handler called when the checked radio menu item changes. + * A function that is called when the checkbox's checked state changes. */ - onChange?: ( event: React.ChangeEvent< HTMLInputElement > ) => void; + onChange?: Ariakit.MenuItemRadioProps[ 'onChange' ]; } export interface MenuSeparatorProps {} diff --git a/packages/components/src/modal/stories/index.story.tsx b/packages/components/src/modal/stories/index.story.tsx index be18ed141dd3fc..3a7b817458ad28 100644 --- a/packages/components/src/modal/stories/index.story.tsx +++ b/packages/components/src/modal/stories/index.story.tsx @@ -75,7 +75,10 @@ const Template: StoryFn< typeof Modal > = ( { onRequestClose, ...args } ) => { anim id est laborum. </p> - <InputControl style={ { marginBottom: '20px' } } /> + <InputControl + __next40pxDefaultSize + style={ { marginBottom: '20px' } } + /> <Button variant="secondary" onClick={ closeModal }> Close Modal diff --git a/packages/components/src/navigation/back-button/index.tsx b/packages/components/src/navigation/back-button/index.tsx index 077e5a8dbdc6d4..ce4a90d9ae7a51 100644 --- a/packages/components/src/navigation/back-button/index.tsx +++ b/packages/components/src/navigation/back-button/index.tsx @@ -49,6 +49,7 @@ function UnforwardedNavigationBackButton( const icon = isRTL() ? chevronRight : chevronLeft; return ( <MenuBackButtonUI + __next40pxDefaultSize className={ classes } href={ href } variant="tertiary" diff --git a/packages/components/src/navigation/index.tsx b/packages/components/src/navigation/index.tsx index 92f431dfb22fc7..ef37caf2f52140 100644 --- a/packages/components/src/navigation/index.tsx +++ b/packages/components/src/navigation/index.tsx @@ -6,6 +6,7 @@ import clsx from 'clsx'; /** * WordPress dependencies */ +import deprecated from '@wordpress/deprecated'; import { useEffect, useRef, useState } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; @@ -79,6 +80,12 @@ export function Navigation( { const navigationTree = useCreateNavigationTree(); const defaultSlideOrigin = isRTL() ? 'right' : 'left'; + deprecated( 'wp.components.Navigation (and all subcomponents)', { + since: '6.8', + version: '7.1', + alternative: 'wp.components.Navigator', + } ); + const setActiveMenu: NavigationContextType[ 'setActiveMenu' ] = ( menuId, slideInOrigin = defaultSlideOrigin diff --git a/packages/components/src/navigation/item/index.tsx b/packages/components/src/navigation/item/index.tsx index 4f4cc2a5dc7a22..160ed36ac63680 100644 --- a/packages/components/src/navigation/item/index.tsx +++ b/packages/components/src/navigation/item/index.tsx @@ -79,6 +79,8 @@ export function NavigationItem( props: NavigationItemProps ) { ? restProps : { as: Button, + __next40pxDefaultSize: + 'as' in restProps ? restProps.as === undefined : true, href, onClick: onItemClick, 'aria-current': isActive ? 'page' : undefined, diff --git a/packages/components/src/navigation/test/index.tsx b/packages/components/src/navigation/test/index.tsx index 20646a6c809bfc..fed939068c0bfd 100644 --- a/packages/components/src/navigation/test/index.tsx +++ b/packages/components/src/navigation/test/index.tsx @@ -176,6 +176,10 @@ describe( 'Navigation', () => { const menuItems = screen.getAllByRole( 'listitem' ); + expect( console ).toHaveWarnedWith( + 'wp.components.Navigation (and all subcomponents) is deprecated since version 6.8 and will be removed in version 7.1. Please use wp.components.Navigator instead.' + ); + expect( menuItems ).toHaveLength( 4 ); expect( menuItems[ 0 ] ).toHaveTextContent( 'Item 1' ); expect( menuItems[ 1 ] ).toHaveTextContent( 'Item 2' ); diff --git a/packages/components/src/navigator/test/index.tsx b/packages/components/src/navigator/test/index.tsx index cab6e9a4cdadff..07b118eaaef70d 100644 --- a/packages/components/src/navigator/test/index.tsx +++ b/packages/components/src/navigator/test/index.tsx @@ -75,6 +75,7 @@ function CustomNavigatorButton( { } ) { return ( <Navigator.Button + __next40pxDefaultSize onClick={ () => { // Used to spy on the values passed to `navigator.goTo`. onClick?.( { type: 'goTo', path } ); @@ -95,6 +96,7 @@ function CustomNavigatorGoToBackButton( { const { goTo } = useNavigator(); return ( <Button + __next40pxDefaultSize onClick={ () => { goTo( path, { isBack: true } ); // Used to spy on the values passed to `navigator.goTo`. @@ -115,6 +117,7 @@ function CustomNavigatorGoToSkipFocusButton( { const { goTo } = useNavigator(); return ( <Button + __next40pxDefaultSize onClick={ () => { goTo( path, { skipFocus: true } ); // Used to spy on the values passed to `navigator.goTo`. @@ -136,6 +139,7 @@ function CustomNavigatorBackButton( { } ) { return ( <Navigator.BackButton + __next40pxDefaultSize onClick={ () => { // Used to spy on the values passed to `navigator.goBack`. onClick?.( { type: 'goBack' } ); diff --git a/packages/components/src/number-control/index.tsx b/packages/components/src/number-control/index.tsx index efa84879b8ff3c..6dd1af4024af7f 100644 --- a/packages/components/src/number-control/index.tsx +++ b/packages/components/src/number-control/index.tsx @@ -242,6 +242,7 @@ function UnforwardedNumberControl( return stateReducerProp?.( baseState, action ) ?? baseState; } } size={ size } + __shouldNotWarnDeprecated36pxSize suffix={ spinControls === 'custom' ? ( <> diff --git a/packages/components/src/palette-edit/index.tsx b/packages/components/src/palette-edit/index.tsx index a58ecbb685e514..2eb6e3bbe3b6fc 100644 --- a/packages/components/src/palette-edit/index.tsx +++ b/packages/components/src/palette-edit/index.tsx @@ -60,6 +60,7 @@ const DEFAULT_COLOR = '#000'; function NameInput( { value, onChange, label }: NameInputProps ) { return ( <NameInputControl + size="compact" label={ label } hideLabelFromVision value={ value } diff --git a/packages/components/src/panel/stories/index.story.tsx b/packages/components/src/panel/stories/index.story.tsx index db66b911d4dc17..1392247036c2cc 100644 --- a/packages/components/src/panel/stories/index.story.tsx +++ b/packages/components/src/panel/stories/index.story.tsx @@ -74,12 +74,12 @@ _PanelRow.args = { children: ( <PanelBody title="My Profile"> <PanelRow> - <InputControl label="First name" /> - <InputControl label="Last name" /> + <InputControl label="First name" __next40pxDefaultSize /> + <InputControl label="Last name" __next40pxDefaultSize /> </PanelRow> <PanelRow> <div style={ { flex: 1 } }> - <InputControl label="Email" /> + <InputControl label="Email" __next40pxDefaultSize /> </div> </PanelRow> </PanelBody> diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index 2ced100dc576be..f5a9ee90519c2d 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -8,6 +8,7 @@ import Theme from './theme'; import { Tabs } from './tabs'; import { kebabCase } from './utils/strings'; import { lock } from './lock-unlock'; +import Badge from './badge'; export const privateApis = {}; lock( privateApis, { @@ -17,4 +18,5 @@ lock( privateApis, { Theme, Menu, kebabCase, + Badge, } ); diff --git a/packages/components/src/radio-group/index.tsx b/packages/components/src/radio-group/index.tsx index e59775c00a8023..589d20ffdaae5b 100644 --- a/packages/components/src/radio-group/index.tsx +++ b/packages/components/src/radio-group/index.tsx @@ -6,6 +6,7 @@ import * as Ariakit from '@ariakit/react'; /** * WordPress dependencies */ +import deprecated from '@wordpress/deprecated'; import { useMemo, forwardRef } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; @@ -46,11 +47,21 @@ function UnforwardedRadioGroup( [ radioStore, disabled ] ); + deprecated( 'wp.components.__experimentalRadioGroup', { + alternative: + 'wp.components.RadioControl or wp.components.__experimentalToggleGroupControl', + since: '6.8', + } ); + return ( <RadioGroupContext.Provider value={ contextValue }> <Ariakit.RadioGroup store={ radioStore } - render={ <ButtonGroup>{ children }</ButtonGroup> } + render={ + <ButtonGroup __shouldNotWarnDeprecated> + { children } + </ButtonGroup> + } aria-label={ label } ref={ ref } { ...props } diff --git a/packages/components/src/radio-group/radio.tsx b/packages/components/src/radio-group/radio.tsx index 782a737b6ba28a..4c54e0694f4bde 100644 --- a/packages/components/src/radio-group/radio.tsx +++ b/packages/components/src/radio-group/radio.tsx @@ -7,7 +7,6 @@ import { forwardRef, useContext } from '@wordpress/element'; * External dependencies */ import * as Ariakit from '@ariakit/react'; -import { useStoreState } from '@ariakit/react'; /** * Internal dependencies @@ -28,7 +27,7 @@ function UnforwardedRadio( ) { const { store, disabled } = useContext( RadioGroupContext ); - const selectedValue = useStoreState( store, 'value' ); + const selectedValue = Ariakit.useStoreState( store, 'value' ); const isChecked = selectedValue !== undefined && selectedValue === value; maybeWarnDeprecated36pxSize( { diff --git a/packages/components/src/range-control/styles/range-control-styles.ts b/packages/components/src/range-control/styles/range-control-styles.ts index d943ca472911ed..c86c57800cac46 100644 --- a/packages/components/src/range-control/styles/range-control-styles.ts +++ b/packages/components/src/range-control/styles/range-control-styles.ts @@ -130,8 +130,10 @@ export const Track = styled.span` margin-top: ${ ( rangeHeightValue - railHeight ) / 2 }px; top: 0; - @media not ( prefers-reduced-motion ) { - transition: width ease 0.1s; + .is-marked & { + @media not ( prefers-reduced-motion ) { + transition: width ease 0.1s; + } } ${ trackBackgroundColor }; @@ -203,8 +205,10 @@ export const ThumbWrapper = styled.span` border-radius: ${ CONFIG.radiusRound }; z-index: 3; - @media not ( prefers-reduced-motion ) { - transition: left ease 0.1s; + .is-marked & { + @media not ( prefers-reduced-motion ) { + transition: left ease 0.1s; + } } ${ thumbColor }; diff --git a/packages/components/src/select-control/README.md b/packages/components/src/select-control/README.md index c240243408fab3..d8742fce74f54e 100644 --- a/packages/components/src/select-control/README.md +++ b/packages/components/src/select-control/README.md @@ -92,6 +92,7 @@ const MySelectControl = () => { { label: 'Small', value: '25%' }, ] } onChange={ ( newSize ) => setSize( newSize ) } + __next40pxDefaultSize __nextHasNoMarginBottom /> ); @@ -114,6 +115,7 @@ Render a user interface to select multiple users from a list. { value: 'b', label: 'User B' }, { value: 'c', label: 'User c' }, ] } + __next40pxDefaultSize __nextHasNoMarginBottom /> ``` @@ -129,6 +131,7 @@ const [ item, setItem ] = useState( '' ); label={ __( 'My dinosaur' ) } value={ item } // e.g: value = 'a' onChange={ ( selection ) => { setItem( selection ) } } + __next40pxDefaultSize __nextHasNoMarginBottom > <optgroup label="Theropods"> diff --git a/packages/components/src/select-control/index.tsx b/packages/components/src/select-control/index.tsx index 3686661b8a58dc..e93e9385a9c23b 100644 --- a/packages/components/src/select-control/index.tsx +++ b/packages/components/src/select-control/index.tsx @@ -18,6 +18,7 @@ import type { WordPressComponentProps } from '../context'; import type { SelectControlProps } from './types'; import SelectControlChevronDown from './chevron-down'; import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props'; +import { maybeWarnDeprecated36pxSize } from '../utils/deprecated-36px-size'; function useUniqueId( idProp?: string ) { const instanceId = useInstanceId( SelectControl ); @@ -65,6 +66,7 @@ function UnforwardedSelectControl< V extends string >( variant = 'default', __next40pxDefaultSize = false, __nextHasNoMarginBottom = false, + __shouldNotWarnDeprecated36pxSize, ...restProps } = useDeprecated36pxDefaultSizeProp( props ); const id = useUniqueId( idProp ); @@ -94,6 +96,13 @@ function UnforwardedSelectControl< V extends string >( const classes = clsx( 'components-select-control', className ); + maybeWarnDeprecated36pxSize( { + componentName: 'SelectControl', + __next40pxDefaultSize, + size, + __shouldNotWarnDeprecated36pxSize, + } ); + return ( <BaseControl help={ help } @@ -154,6 +163,7 @@ function UnforwardedSelectControl< V extends string >( * * return ( * <SelectControl + * __next40pxDefaultSize * __nextHasNoMarginBottom * label="Size" * value={ size } diff --git a/packages/components/src/select-control/stories/index.story.tsx b/packages/components/src/select-control/stories/index.story.tsx index d872df540602e4..e9f7f09a1ddb05 100644 --- a/packages/components/src/select-control/stories/index.story.tsx +++ b/packages/components/src/select-control/stories/index.story.tsx @@ -65,6 +65,7 @@ const SelectControlWithState: StoryFn< typeof SelectControl > = ( props ) => { export const Default = SelectControlWithState.bind( {} ); Default.args = { + __next40pxDefaultSize: true, __nextHasNoMarginBottom: true, label: 'Label', options: [ @@ -87,6 +88,7 @@ WithLabelAndHelpText.args = { */ export const WithCustomChildren = SelectControlWithState.bind( {} ); WithCustomChildren.args = { + __next40pxDefaultSize: true, __nextHasNoMarginBottom: true, label: 'Label', children: ( diff --git a/packages/components/src/select-control/test/select-control.tsx b/packages/components/src/select-control/test/select-control.tsx index 47b684cd20e280..37935d60384b15 100644 --- a/packages/components/src/select-control/test/select-control.tsx +++ b/packages/components/src/select-control/test/select-control.tsx @@ -12,7 +12,13 @@ import _SelectControl from '..'; const SelectControl = ( props: React.ComponentProps< typeof _SelectControl > ) => { - return <_SelectControl { ...props } __nextHasNoMarginBottom />; + return ( + <_SelectControl + { ...props } + __nextHasNoMarginBottom + __next40pxDefaultSize + /> + ); }; describe( 'SelectControl', () => { diff --git a/packages/components/src/select-control/types.ts b/packages/components/src/select-control/types.ts index 4e7211ab9abfb2..3d9f06385c7532 100644 --- a/packages/components/src/select-control/types.ts +++ b/packages/components/src/select-control/types.ts @@ -13,6 +13,7 @@ type SelectControlBaseProps< V extends string > = Pick< InputBaseProps, | '__next36pxDefaultSize' | '__next40pxDefaultSize' + | '__shouldNotWarnDeprecated36pxSize' | 'disabled' | 'hideLabelFromVision' | 'label' diff --git a/packages/components/src/slot-fill/context.ts b/packages/components/src/slot-fill/context.ts index c4839462fbce0c..b1f0718180e9eb 100644 --- a/packages/components/src/slot-fill/context.ts +++ b/packages/components/src/slot-fill/context.ts @@ -1,20 +1,22 @@ /** * WordPress dependencies */ +import { observableMap } from '@wordpress/compose'; import { createContext } from '@wordpress/element'; + /** * Internal dependencies */ import type { BaseSlotFillContext } from './types'; const initialValue: BaseSlotFillContext = { + slots: observableMap(), + fills: observableMap(), registerSlot: () => {}, unregisterSlot: () => {}, registerFill: () => {}, unregisterFill: () => {}, - getSlot: () => undefined, - getFills: () => [], - subscribe: () => () => {}, + updateFill: () => {}, }; export const SlotFillContext = createContext( initialValue ); diff --git a/packages/components/src/slot-fill/fill.ts b/packages/components/src/slot-fill/fill.ts index 0a31c8276b3f10..0bd1aec8fa3e0e 100644 --- a/packages/components/src/slot-fill/fill.ts +++ b/packages/components/src/slot-fill/fill.ts @@ -7,31 +7,26 @@ import { useContext, useLayoutEffect, useRef } from '@wordpress/element'; * Internal dependencies */ import SlotFillContext from './context'; -import useSlot from './use-slot'; import type { FillComponentProps } from './types'; export default function Fill( { name, children }: FillComponentProps ) { const registry = useContext( SlotFillContext ); - const slot = useSlot( name ); + const instanceRef = useRef( {} ); + const childrenRef = useRef( children ); - const ref = useRef( { - name, - children, - } ); + useLayoutEffect( () => { + childrenRef.current = children; + }, [ children ] ); useLayoutEffect( () => { - const refValue = ref.current; - refValue.name = name; - registry.registerFill( name, refValue ); - return () => registry.unregisterFill( name, refValue ); + const instance = instanceRef.current; + registry.registerFill( name, instance, childrenRef.current ); + return () => registry.unregisterFill( name, instance ); }, [ registry, name ] ); useLayoutEffect( () => { - ref.current.children = children; - if ( slot ) { - slot.rerender(); - } - }, [ slot, children ] ); + registry.updateFill( name, instanceRef.current, childrenRef.current ); + } ); return null; } diff --git a/packages/components/src/slot-fill/provider.tsx b/packages/components/src/slot-fill/provider.tsx index e2b98e73e1b707..e5319bc7f33e44 100644 --- a/packages/components/src/slot-fill/provider.tsx +++ b/packages/components/src/slot-fill/provider.tsx @@ -8,103 +8,102 @@ import { useState } from '@wordpress/element'; */ import SlotFillContext from './context'; import type { - FillComponentProps, + FillInstance, + FillChildren, + BaseSlotInstance, BaseSlotFillContext, SlotFillProviderProps, SlotKey, - Rerenderable, } from './types'; +import { observableMap } from '@wordpress/compose'; function createSlotRegistry(): BaseSlotFillContext { - const slots: Record< SlotKey, Rerenderable > = {}; - const fills: Record< SlotKey, FillComponentProps[] > = {}; - let listeners: Array< () => void > = []; - - function registerSlot( name: SlotKey, slot: Rerenderable ) { - const previousSlot = slots[ name ]; - slots[ name ] = slot; - triggerListeners(); - - // Sometimes the fills are registered after the initial render of slot - // But before the registerSlot call, we need to rerender the slot. - forceUpdateSlot( name ); - - // If a new instance of a slot is being mounted while another with the - // same name exists, force its update _after_ the new slot has been - // assigned into the instance, such that its own rendering of children - // will be empty (the new Slot will subsume all fills for this name). - if ( previousSlot ) { - previousSlot.rerender(); - } - } - - function registerFill( name: SlotKey, instance: FillComponentProps ) { - fills[ name ] = [ ...( fills[ name ] || [] ), instance ]; - forceUpdateSlot( name ); + const slots = observableMap< SlotKey, BaseSlotInstance >(); + const fills = observableMap< + SlotKey, + { instance: FillInstance; children: FillChildren }[] + >(); + + function registerSlot( name: SlotKey, instance: BaseSlotInstance ) { + slots.set( name, instance ); } - function unregisterSlot( name: SlotKey, instance: Rerenderable ) { + function unregisterSlot( name: SlotKey, instance: BaseSlotInstance ) { // If a previous instance of a Slot by this name unmounts, do nothing, // as the slot and its fills should only be removed for the current // known instance. - if ( slots[ name ] !== instance ) { + if ( slots.get( name ) !== instance ) { return; } - delete slots[ name ]; - triggerListeners(); + slots.delete( name ); } - function unregisterFill( name: SlotKey, instance: FillComponentProps ) { - fills[ name ] = - fills[ name ]?.filter( ( fill ) => fill !== instance ) ?? []; - forceUpdateSlot( name ); + function registerFill( + name: SlotKey, + instance: FillInstance, + children: FillChildren + ) { + fills.set( name, [ + ...( fills.get( name ) || [] ), + { instance, children }, + ] ); } - function getSlot( name: SlotKey ): Rerenderable | undefined { - return slots[ name ]; + function unregisterFill( name: SlotKey, instance: FillInstance ) { + const fillsForName = fills.get( name ); + if ( ! fillsForName ) { + return; + } + + fills.set( + name, + fillsForName.filter( ( fill ) => fill.instance !== instance ) + ); } - function getFills( + function updateFill( name: SlotKey, - slotInstance: Rerenderable - ): FillComponentProps[] { - // Fills should only be returned for the current instance of the slot - // in which they occupy. - if ( slots[ name ] !== slotInstance ) { - return []; + instance: FillInstance, + children: FillChildren + ) { + const fillsForName = fills.get( name ); + if ( ! fillsForName ) { + return; } - return fills[ name ]; - } - - function forceUpdateSlot( name: SlotKey ) { - const slot = getSlot( name ); - if ( slot ) { - slot.rerender(); + const fillForInstance = fillsForName.find( + ( f ) => f.instance === instance + ); + if ( ! fillForInstance ) { + return; } - } - function triggerListeners() { - listeners.forEach( ( listener ) => listener() ); - } - - function subscribe( listener: () => void ) { - listeners.push( listener ); + if ( fillForInstance.children === children ) { + return; + } - return () => { - listeners = listeners.filter( ( l ) => l !== listener ); - }; + fills.set( + name, + fillsForName.map( ( f ) => { + if ( f.instance === instance ) { + // Replace with new record with updated `children`. + return { instance, children }; + } + + return f; + } ) + ); } return { + slots, + fills, registerSlot, unregisterSlot, registerFill, unregisterFill, - getSlot, - getFills, - subscribe, + updateFill, }; } diff --git a/packages/components/src/slot-fill/slot.tsx b/packages/components/src/slot-fill/slot.tsx index fe4a741ddbfbad..c1182562672c0b 100644 --- a/packages/components/src/slot-fill/slot.tsx +++ b/packages/components/src/slot-fill/slot.tsx @@ -6,10 +6,10 @@ import type { ReactElement, ReactNode, Key } from 'react'; /** * WordPress dependencies */ +import { useObservableValue } from '@wordpress/compose'; import { useContext, - useEffect, - useReducer, + useLayoutEffect, useRef, Children, cloneElement, @@ -32,41 +32,48 @@ function isFunction( maybeFunc: any ): maybeFunc is Function { return typeof maybeFunc === 'function'; } +function addKeysToChildren( children: ReactNode ) { + return Children.map( children, ( child, childIndex ) => { + if ( ! child || typeof child === 'string' ) { + return child; + } + let childKey: Key = childIndex; + if ( typeof child === 'object' && 'key' in child && child?.key ) { + childKey = child.key; + } + + return cloneElement( child as ReactElement, { + key: childKey, + } ); + } ); +} + function Slot( props: Omit< SlotComponentProps, 'bubblesVirtually' > ) { const registry = useContext( SlotFillContext ); - const [ , rerender ] = useReducer( () => [], [] ); - const ref = useRef( { rerender } ); + const instanceRef = useRef( {} ); const { name, children, fillProps = {} } = props; - useEffect( () => { - const refValue = ref.current; - registry.registerSlot( name, refValue ); - return () => registry.unregisterSlot( name, refValue ); + useLayoutEffect( () => { + const instance = instanceRef.current; + registry.registerSlot( name, instance ); + return () => registry.unregisterSlot( name, instance ); }, [ registry, name ] ); - const fills: ReactNode[] = ( registry.getFills( name, ref.current ) ?? [] ) + let fills = useObservableValue( registry.fills, name ) ?? []; + const currentSlot = useObservableValue( registry.slots, name ); + + // Fills should only be rendered in the currently registered instance of the slot. + if ( currentSlot !== instanceRef.current ) { + fills = []; + } + + const renderedFills = fills .map( ( fill ) => { const fillChildren = isFunction( fill.children ) ? fill.children( fillProps ) : fill.children; - return Children.map( fillChildren, ( child, childIndex ) => { - if ( ! child || typeof child === 'string' ) { - return child; - } - let childKey: Key = childIndex; - if ( - typeof child === 'object' && - 'key' in child && - child?.key - ) { - childKey = child.key; - } - - return cloneElement( child as ReactElement, { - key: childKey, - } ); - } ); + return addKeysToChildren( fillChildren ); } ) .filter( // In some cases fills are rendered only when some conditions apply. @@ -75,7 +82,13 @@ function Slot( props: Omit< SlotComponentProps, 'bubblesVirtually' > ) { ( element ) => ! isEmptyElement( element ) ); - return <>{ isFunction( children ) ? children( fills ) : fills }</>; + return ( + <> + { isFunction( children ) + ? children( renderedFills ) + : renderedFills } + </> + ); } export default Slot; diff --git a/packages/components/src/slot-fill/types.ts b/packages/components/src/slot-fill/types.ts index 6668057323edd9..758f1c8257d548 100644 --- a/packages/components/src/slot-fill/types.ts +++ b/packages/components/src/slot-fill/types.ts @@ -84,6 +84,10 @@ export type SlotComponentProps = style?: never; } ); +export type FillChildren = + | ReactNode + | ( ( fillProps: FillProps ) => ReactNode ); + export type FillComponentProps = { /** * The name of the slot to fill into. @@ -93,7 +97,7 @@ export type FillComponentProps = { /** * Children elements or render function. */ - children?: ReactNode | ( ( fillProps: FillProps ) => ReactNode ); + children?: FillChildren; }; export type SlotFillProviderProps = { @@ -109,8 +113,8 @@ export type SlotFillProviderProps = { }; export type SlotRef = RefObject< HTMLElement >; -export type Rerenderable = { rerender: () => void }; export type FillInstance = {}; +export type BaseSlotInstance = {}; export type SlotFillBubblesVirtuallyContext = { slots: ObservableMap< SlotKey, { ref: SlotRef; fillProps: FillProps } >; @@ -128,14 +132,22 @@ export type SlotFillBubblesVirtuallyContext = { }; export type BaseSlotFillContext = { - registerSlot: ( name: SlotKey, slot: Rerenderable ) => void; - unregisterSlot: ( name: SlotKey, slot: Rerenderable ) => void; - registerFill: ( name: SlotKey, instance: FillComponentProps ) => void; - unregisterFill: ( name: SlotKey, instance: FillComponentProps ) => void; - getSlot: ( name: SlotKey ) => Rerenderable | undefined; - getFills: ( + slots: ObservableMap< SlotKey, BaseSlotInstance >; + fills: ObservableMap< + SlotKey, + { instance: FillInstance; children: FillChildren }[] + >; + registerSlot: ( name: SlotKey, slot: BaseSlotInstance ) => void; + unregisterSlot: ( name: SlotKey, slot: BaseSlotInstance ) => void; + registerFill: ( + name: SlotKey, + instance: FillInstance, + children: FillChildren + ) => void; + unregisterFill: ( name: SlotKey, instance: FillInstance ) => void; + updateFill: ( name: SlotKey, - slotInstance: Rerenderable - ) => FillComponentProps[]; - subscribe: ( listener: () => void ) => () => void; + instance: FillInstance, + children: FillChildren + ) => void; }; diff --git a/packages/components/src/slot-fill/use-slot.ts b/packages/components/src/slot-fill/use-slot.ts deleted file mode 100644 index 4ab419be1ad2bd..00000000000000 --- a/packages/components/src/slot-fill/use-slot.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * WordPress dependencies - */ -import { useContext, useSyncExternalStore } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import SlotFillContext from './context'; -import type { SlotKey } from './types'; - -/** - * React hook returning the active slot given a name. - * - * @param name Slot name. - * @return Slot object. - */ -const useSlot = ( name: SlotKey ) => { - const { getSlot, subscribe } = useContext( SlotFillContext ); - return useSyncExternalStore( - subscribe, - () => getSlot( name ), - () => getSlot( name ) - ); -}; - -export default useSlot; diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index 70317f4a2d0e0b..368dec0f5e253d 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -10,6 +10,7 @@ // Components @import "./animate/style.scss"; @import "./autocomplete/style.scss"; +@import "./badge/styles.scss"; @import "./button-group/style.scss"; @import "./button/style.scss"; @import "./checkbox-control/style.scss"; diff --git a/packages/components/src/tab-panel/index.tsx b/packages/components/src/tab-panel/index.tsx index be06b42fcd013f..ec4f33d875a381 100644 --- a/packages/components/src/tab-panel/index.tsx +++ b/packages/components/src/tab-panel/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import * as Ariakit from '@ariakit/react'; -import { useStoreState } from '@ariakit/react'; import clsx from 'clsx'; import type { ForwardedRef } from 'react'; @@ -125,7 +124,7 @@ const UnforwardedTabPanel = ( } ); const selectedTabName = extractTabName( - useStoreState( tabStore, 'selectedId' ) + Ariakit.useStoreState( tabStore, 'selectedId' ) ); const setTabStoreSelectedId = useCallback( diff --git a/packages/components/src/tab-panel/stories/index.story.tsx b/packages/components/src/tab-panel/stories/index.story.tsx index 57a3cc311f863c..8f40c61beb5239 100644 --- a/packages/components/src/tab-panel/stories/index.story.tsx +++ b/packages/components/src/tab-panel/stories/index.story.tsx @@ -2,6 +2,7 @@ * External dependencies */ import type { Meta, StoryFn } from '@storybook/react'; +import { fn } from '@storybook/test'; /** * WordPress dependencies @@ -22,6 +23,9 @@ const meta: Meta< typeof TabPanel > = { controls: { expanded: true }, docs: { canvas: { sourceState: 'shown' } }, }, + args: { + onSelect: fn(), + }, }; export default meta; diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md index 9c7e846046c904..7f5f3219adfd1e 100644 --- a/packages/components/src/tabs/README.md +++ b/packages/components/src/tabs/README.md @@ -1,254 +1,218 @@ # Tabs -<div class="callout callout-alert"> -This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. -</div> - -Tabs is a collection of React components that combine to render an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). - -Tabs organizes content across different screens, data sets, and interactions. It has two sections: a list of tabs, and the view to show when tabs are chosen. - -## Development guidelines - -### Usage - -#### Uncontrolled Mode - -Tabs can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultTabId` prop can be used to set the initially selected tab. If this prop is not set, the first tab will be selected by default. In addition, in most cases where the currently active tab becomes disabled or otherwise unavailable, uncontrolled mode will automatically fall back to selecting the first available tab. - -```jsx -import { Tabs } from '@wordpress/components'; - -const onSelect = ( tabName ) => { - console.log( 'Selecting tab', tabName ); -}; - -const MyUncontrolledTabs = () => ( - <Tabs onSelect={ onSelect } defaultTabId="tab2"> - <Tabs.TabList> - <Tabs.Tab tabId="tab1" title="Tab 1"> - Tab 1 - </Tabs.Tab> - <Tabs.Tab tabId="tab2" title="Tab 2"> - Tab 2 - </Tabs.Tab> - <Tabs.Tab tabId="tab3" title="Tab 3"> - Tab 3 - </Tabs.Tab> - </Tabs.TabList> - <Tabs.TabPanel tabId="tab1"> - <p>Selected tab: Tab 1</p> - </Tabs.TabPanel> - <Tabs.TabPanel tabId="tab2"> - <p>Selected tab: Tab 2</p> - </Tabs.TabPanel> - <Tabs.TabPanel tabId="tab3"> - <p>Selected tab: Tab 3</p> - </Tabs.TabPanel> - </Tabs> - ); -``` - -#### Controlled Mode - -Tabs can also be used in a controlled mode, where the parent component specifies the `selectedTabId` and the `onSelect` props to control tab selection. In this mode, the `defaultTabId` prop will be ignored if it is provided. If the `selectedTabId` is `null`, no tab is selected. In this mode, if the currently selected tab becomes disabled or otherwise unavailable, the component will _not_ fall back to another available tab, leaving the controlling component in charge of implementing the desired logic. - -```jsx -import { Tabs } from '@wordpress/components'; - const [ selectedTabId, setSelectedTabId ] = useState< - string | undefined | null - >(); - -const onSelect = ( tabName ) => { - console.log( 'Selecting tab', tabName ); -}; - -const MyControlledTabs = () => ( - <Tabs - selectedTabId={ selectedTabId } - onSelect={ ( selectedId ) => { - setSelectedTabId( selectedId ); - onSelect( selectedId ); - } } - > - <Tabs.TabList> - <Tabs.Tab tabId="tab1" title="Tab 1"> - Tab 1 - </Tabs.Tab> - <Tabs.Tab tabId="tab2" title="Tab 2"> - Tab 2 - </Tabs.Tab> - <Tabs.Tab tabId="tab3" title="Tab 3"> - Tab 3 - </Tabs.Tab> - </Tabs.TabList> - <Tabs.TabPanel tabId="tab1"> - <p>Selected tab: Tab 1</p> - </Tabs.TabPanel> - <Tabs.TabPanel tabId="tab2"> - <p>Selected tab: Tab 2</p> - </Tabs.TabPanel> - <Tabs.TabPanel tabId="tab3"> - <p>Selected tab: Tab 3</p> - </Tabs.TabPanel> - </Tabs> - ); -``` - -### Components and Sub-components - -Tabs is comprised of four individual components: -- `Tabs`: a wrapper component and context provider. It is responsible for managing the state of the tabs and rendering the `TabList` and `TabPanels`. -- `TabList`: a wrapper component for the `Tab` components. It is responsible for rendering the list of tabs. -- `Tab`: renders a single tab. The currently active tab receives default styling that can be overridden with CSS targeting [aria-selected="true"]. -- `TabPanel`: renders the content to display for a single tab once that tab is selected. - -#### Tabs - -##### Props - -###### `children`: `React.ReactNode` - -The children elements, which should include one instance of the `Tabs.Tablist` component and as many instances of the `Tabs.TabPanel` components as there are `Tabs.Tab` components. - -- Required: Yes - -###### `selectOnMove`: `boolean` - -Determines if the tab should be selected when it receives focus. If set to `false`, the tab will only be selected upon clicking, not when using arrow keys to shift focus (manual tab activation). See the [official W3C docs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) for more info. - -- Required: No -- Default: `true` - -###### `selectedTabId`: `string | null` +<!-- This file is generated automatically and cannot be edited directly. Make edits via TypeScript types and TSDocs. --> -The id of the tab whose panel is currently visible. +🔒 This component is locked as a [private API](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-private-apis/). We do not yet recommend using this outside of the Gutenberg project. -If left `undefined`, it will be automatically set to the first enabled tab, and the component assumes it is being used in "uncontrolled" mode. +<p class="callout callout-info">See the <a href="https://wordpress.github.io/gutenberg/?path=/docs/components-tabs--docs">WordPress Storybook</a> for more detailed, interactive documentation.</p> -Consequently, any value different than `undefined` will set the component in "controlled" mode. When in "controlled" mode, the `null` value will result in no tabs being selected, and the tablist becoming tabbable. +Tabs is a collection of React components that combine to render +an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). -- Required: No +Tabs organizes content across different screens, data sets, and interactions. +It has two sections: a list of tabs, and the view to show when a tab is chosen. -###### `defaultTabId`: `string | null` +`Tabs` itself is a wrapper component and context provider. +It is responsible for managing the state of the tabs, and rendering one instance of the `Tabs.TabList` component and one or more instances of the `Tab.TabPanel` component. -The id of the tab whose panel is currently visible. +## Props -If left `undefined`, it will be automatically set to the first enabled tab. If set to `null`, no tab will be selected, and the tablist will be tabbable. +### `activeTabId` -_Note: this prop will be overridden by the `selectedTabId` prop if it is provided (meaning the component will be used in "controlled" mode)._ + - Type: `string` + - Required: No -- Required: No +The current active tab `id`. The active tab is the tab element within the +tablist widget that has DOM focus. -###### `onSelect`: `( ( selectedId: string | null | undefined ) => void )` +- `null` represents the tablist (ie. the base composite element). Users + will be able to navigate out of it using arrow keys. +- If `activeTabId` is initially set to `null`, the base composite element + itself will have focus and users will be able to navigate to it using + arrow keys. -The function called when the `selectedTabId` changes. +### `children` -- Required: No -- Default: `noop` + - Type: `ReactNode` + - Required: Yes -###### `activeTabId`: `string | null` +The children elements, which should include one instance of the +`Tabs.Tablist` component and as many instances of the `Tabs.TabPanel` +components as there are `Tabs.Tab` components. -The current active tab `id`. The active tab is the tab element within the tablist widget that has DOM focus. +### `defaultTabId` -- `null` represents the tablist (ie. the base composite element). Users - will be able to navigate out of it using arrow keys; -- If `activeTabId` is initially set to `null`, the base composite element - itself will have focus and users will be able to navigate to it using - arrow keys. + - Type: `string` + - Required: No + +The id of the tab whose panel is currently visible. -- Required: No +If left `undefined`, it will be automatically set to the first enabled +tab. If set to `null`, no tab will be selected, and the tablist will be +tabbable. -###### `defaultActiveTabId`: `string | null` +Note: this prop will be overridden by the `selectedTabId` prop if it is +provided (meaning the component will be used in "controlled" mode). -The tab id that should be active by default when the composite widget is rendered. If `null`, the tablist element itself will have focus and users will be able to navigate to it using arrow keys. If `undefined`, the first enabled item will be focused. +### `defaultActiveTabId` -_Note: this prop will be overridden by the `activeTabId` prop if it is provided._ + - Type: `string` + - Required: No -- Required: No +The tab id that should be active by default when the composite widget is +rendered. If `null`, the tablist element itself will have focus +and users will be able to navigate to it using arrow keys. If `undefined`, +the first enabled item will be focused. -###### `onActiveTabIdChange`: `( ( activeId: string | null | undefined ) => void )` +Note: this prop will be overridden by the `activeTabId` prop if it is +provided. + +### `onSelect` + + - Type: `(selectedId: string) => void` + - Required: No The function called when the `selectedTabId` changes. -- Required: No -- Default: `noop` +### `onActiveTabIdChange` + + - Type: `(activeId: string) => void` + - Required: No + +A callback that gets called when the `activeTabId` state changes. -###### `orientation`: `'horizontal' | 'vertical' | 'both'` +### `orientation` -Defines the orientation of the tablist and determines which arrow keys can be used to move focus: + - Type: `"horizontal" | "vertical" | "both"` + - Required: No + - Default: `"horizontal"` -- `both`: all arrow keys work; -- `horizontal`: only left and right arrow keys work; +Defines the orientation of the tablist and determines which arrow keys +can be used to move focus: + +- `both`: all arrow keys work. +- `horizontal`: only left and right arrow keys work. - `vertical`: only up and down arrow keys work. -- Required: No -- Default: `horizontal` +### `selectOnMove` + + - Type: `boolean` + - Required: No + - Default: `true` + +Determines if the tab should be selected when it receives focus. If set to +`false`, the tab will only be selected upon clicking, not when using arrow +keys to shift focus (manual tab activation). See the [official W3C docs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) +for more info. + +### `selectedTabId` + + - Type: `string` + - Required: No + +The id of the tab whose panel is currently visible. + +If left `undefined`, it will be automatically set to the first enabled +tab, and the component assumes it is being used in "uncontrolled" mode. -#### TabList +Consequently, any value different than `undefined` will set the component +in "controlled" mode. When in "controlled" mode, the `null` value will +result in no tabs being selected, and the tablist becoming tabbable. -##### Props +## Subcomponents -###### `children`: `React.ReactNode` +### Tabs.TabList -The children elements, which should include one or more instances of the `Tabs.Tab` component. +A wrapper component for the `Tab` components. -- Required: No +It is responsible for rendering the list of tabs. -#### Tab +#### Props -##### Props +##### `children` -###### `tabId`: `string` + - Type: `ReactNode` + - Required: Yes -The unique ID of the tab. It will be used to register the tab and match it to a corresponding `Tabs.TabPanel` component. If not provided, a unique ID will be automatically generated. +The children elements, which should include one or more instances of the +`Tabs.Tab` component. -- Required: Yes +### Tabs.Tab -###### `children`: `React.ReactNode` +Renders a single tab. + +The currently active tab receives default styling that can be +overridden with CSS targeting `[aria-selected="true"]`. + +#### Props + +##### `children` + + - Type: `ReactNode` + - Required: No The contents of the tab. -- Required: No +##### `disabled` -###### `disabled`: `boolean` + - Type: `boolean` + - Required: No + - Default: `false` -Determines if the tab should be disabled. Note that disabled tabs can still be accessed via the keyboard when navigating through the tablist. +Determines if the tab should be disabled. Note that disabled tabs can +still be accessed via the keyboard when navigating through the tablist. -- Required: No -- Default: `false` +##### `render` -###### `render`: `React.ReactNode` + - Type: `RenderProp<HTMLAttributes<any> & { ref?: Ref<any>; }> | ReactElement<any, string | JSXElementConstructor<any>>` + - Required: No -Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. +Allows the component to be rendered as a different HTML element or React +component. The value can be a React element or a function that takes in the +original component props and gives back a React element with the props +merged. By default, the tab will be rendered as a `button` element. -- Required: No +##### `tabId` -#### TabPanel + - Type: `string` + - Required: Yes -##### Props +The unique ID of the tab. It will be used to register the tab and match +it to a corresponding `Tabs.TabPanel` component. -###### `children`: `React.ReactNode` +### Tabs.TabPanel -The contents of the tab panel. +Renders the content to display for a single tab once that tab is selected. -- Required: No +#### Props -###### `tabId`: `string` +##### `children` -The unique `id` of the `Tabs.Tab` component controlling this panel. This connection is used to assign the `aria-labelledby` attribute to the tab panel and to determine if the tab panel should be visible. + - Type: `ReactNode` + - Required: No -If not provided, this link is automatically established by matching the order of `Tabs.Tab` and `Tabs.TabPanel` elements in the DOM. +The contents of the tab panel. -- Required: Yes +##### `focusable` -###### `focusable`: `boolean` + - Type: `boolean` + - Required: No + - Default: `true` Determines whether or not the tabpanel element should be focusable. +If `false`, pressing the tab key will skip over the tabpanel, and instead +focus on the first focusable element in the panel (if there is one). + +##### `tabId` + + - Type: `string` + - Required: Yes -If `false`, pressing the tab key will skip over the tabpanel, and instead focus on the first focusable element in the panel (if there is one). +The unique `id` of the `Tabs.Tab` component controlling this panel. This +connection is used to assign the `aria-labelledby` attribute to the tab +panel and to determine if the tab panel should be visible. -- Required: No -- Default: `true` +If not provided, this link is automatically established by matching the +order of `Tabs.Tab` and `Tabs.TabPanel` elements in the DOM. diff --git a/packages/components/src/tabs/docs-manifest.json b/packages/components/src/tabs/docs-manifest.json new file mode 100644 index 00000000000000..fc24b177ef6163 --- /dev/null +++ b/packages/components/src/tabs/docs-manifest.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../schemas/docs-manifest.json", + "displayName": "Tabs", + "filePath": "./index.tsx", + "subcomponents": [ + { + "displayName": "TabList", + "preferredDisplayName": "Tabs.TabList", + "filePath": "./tablist.tsx" + }, + { + "displayName": "Tab", + "preferredDisplayName": "Tabs.Tab", + "filePath": "./tab.tsx" + }, + { + "displayName": "TabPanel", + "preferredDisplayName": "Tabs.TabPanel", + "filePath": "./tabpanel.tsx" + } + ] +} diff --git a/packages/components/src/tabs/index.tsx b/packages/components/src/tabs/index.tsx index 819d259395daf8..2cbe487976c59e 100644 --- a/packages/components/src/tabs/index.tsx +++ b/packages/components/src/tabs/index.tsx @@ -36,11 +36,14 @@ function internalToExternalTabId( } /** - * Display one panel of content at a time with a tabbed interface, based on the - * WAI-ARIA Tabs Pattern⁠. + * Tabs is a collection of React components that combine to render + * an [ARIA-compliant tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). * - * @see https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ - * ``` + * Tabs organizes content across different screens, data sets, and interactions. + * It has two sections: a list of tabs, and the view to show when a tab is chosen. + * + * `Tabs` itself is a wrapper component and context provider. + * It is responsible for managing the state of the tabs, and rendering one instance of the `Tabs.TabList` component and one or more instances of the `Tab.TabPanel` component. */ export const Tabs = Object.assign( function Tabs( { @@ -121,12 +124,26 @@ export const Tabs = Object.assign( ); }, { + /** + * Renders a single tab. + * + * The currently active tab receives default styling that can be + * overridden with CSS targeting `[aria-selected="true"]`. + */ Tab: Object.assign( Tab, { displayName: 'Tabs.Tab', } ), + /** + * A wrapper component for the `Tab` components. + * + * It is responsible for rendering the list of tabs. + */ TabList: Object.assign( TabList, { displayName: 'Tabs.TabList', } ), + /** + * Renders the content to display for a single tab once that tab is selected. + */ TabPanel: Object.assign( TabPanel, { displayName: 'Tabs.TabPanel', } ), diff --git a/packages/components/src/tabs/stories/best-practices.mdx b/packages/components/src/tabs/stories/best-practices.mdx new file mode 100644 index 00000000000000..a8bb9cf20a5f0e --- /dev/null +++ b/packages/components/src/tabs/stories/best-practices.mdx @@ -0,0 +1,99 @@ +import { Meta } from '@storybook/blocks'; + +import * as TabsStories from './index.story'; + +<Meta of={ TabsStories } name="Best Practices" /> + +# Tabs + +## Usage + +### Uncontrolled Mode + +Tabs can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultTabId` prop can be used to set the initially selected tab. If this prop is not set, the first tab will be selected by default. In addition, in most cases where the currently active tab becomes disabled or otherwise unavailable, uncontrolled mode will automatically fall back to selecting the first available tab. + +```jsx +import { Tabs } from '@wordpress/components'; + +const onSelect = ( tabName ) => { + console.log( 'Selecting tab', tabName ); +}; + +const MyUncontrolledTabs = () => ( + <Tabs onSelect={ onSelect } defaultTabId="tab2"> + <Tabs.TabList> + <Tabs.Tab tabId="tab1" title="Tab 1"> + Tab 1 + </Tabs.Tab> + <Tabs.Tab tabId="tab2" title="Tab 2"> + Tab 2 + </Tabs.Tab> + <Tabs.Tab tabId="tab3" title="Tab 3"> + Tab 3 + </Tabs.Tab> + </Tabs.TabList> + <Tabs.TabPanel tabId="tab1"> + <p>Selected tab: Tab 1</p> + </Tabs.TabPanel> + <Tabs.TabPanel tabId="tab2"> + <p>Selected tab: Tab 2</p> + </Tabs.TabPanel> + <Tabs.TabPanel tabId="tab3"> + <p>Selected tab: Tab 3</p> + </Tabs.TabPanel> + </Tabs> +); +``` + +### Controlled Mode + +Tabs can also be used in a controlled mode, where the parent component specifies the `selectedTabId` and the `onSelect` props to control tab selection. In this mode, the `defaultTabId` prop will be ignored if it is provided. If the `selectedTabId` is `null`, no tab is selected. In this mode, if the currently selected tab becomes disabled or otherwise unavailable, the component will _not_ fall back to another available tab, leaving the controlling component in charge of implementing the desired logic. + +```tsx +import { Tabs } from '@wordpress/components'; + +const [ selectedTabId, setSelectedTabId ] = useState< + string | undefined | null +>(); + +const onSelect = ( tabName ) => { + console.log( 'Selecting tab', tabName ); +}; + +const MyControlledTabs = () => ( + <Tabs + selectedTabId={ selectedTabId } + onSelect={ ( selectedId ) => { + setSelectedTabId( selectedId ); + onSelect( selectedId ); + } } + > + <Tabs.TabList> + <Tabs.Tab tabId="tab1" title="Tab 1"> + Tab 1 + </Tabs.Tab> + <Tabs.Tab tabId="tab2" title="Tab 2"> + Tab 2 + </Tabs.Tab> + <Tabs.Tab tabId="tab3" title="Tab 3"> + Tab 3 + </Tabs.Tab> + </Tabs.TabList> + <Tabs.TabPanel tabId="tab1"> + <p>Selected tab: Tab 1</p> + </Tabs.TabPanel> + <Tabs.TabPanel tabId="tab2"> + <p>Selected tab: Tab 2</p> + </Tabs.TabPanel> + <Tabs.TabPanel tabId="tab3"> + <p>Selected tab: Tab 3</p> + </Tabs.TabPanel> + </Tabs> +); +``` + +### Using `Tabs` with links + +The semantics implemented by the `Tabs` component don't align well with the semantics needed by a list of links. Furthermore, end users usually expect every link to be tabbable, while `Tabs.Tablist` is a [composite](https://w3c.github.io/aria/#composite) widget acting as a single tab stop. + +For these reasons, even if the `Tabs` component is fully extensible, we don't recommend using `Tabs` with links, and we don't currently provide any related Storybook example. diff --git a/packages/components/src/tabs/stories/index.story.tsx b/packages/components/src/tabs/stories/index.story.tsx index 5b2fd621bbb436..e434bb501d85c9 100644 --- a/packages/components/src/tabs/stories/index.story.tsx +++ b/packages/components/src/tabs/stories/index.story.tsx @@ -2,6 +2,7 @@ * External dependencies */ import type { Meta, StoryFn } from '@storybook/react'; +import { fn } from '@storybook/test'; /** * WordPress dependencies @@ -39,6 +40,10 @@ const meta: Meta< typeof Tabs > = { controls: { expanded: true }, docs: { canvas: { sourceState: 'shown' } }, }, + args: { + onActiveTabIdChange: fn(), + onSelect: fn(), + }, }; export default meta; diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts index 959a82509a05d6..7ef0f919322c04 100644 --- a/packages/components/src/tabs/types.ts +++ b/packages/components/src/tabs/types.ts @@ -22,18 +22,16 @@ export type TabsProps = { * `Tabs.Tablist` component and as many instances of the `Tabs.TabPanel` * components as there are `Tabs.Tab` components. */ - children: Ariakit.TabProps[ 'children' ]; + children: Ariakit.TabProviderProps[ 'children' ]; /** * Determines if the tab should be selected when it receives focus. If set to * `false`, the tab will only be selected upon clicking, not when using arrow - * keys to shift focus (manual tab activation). See the official W3C docs + * keys to shift focus (manual tab activation). See the [official W3C docs](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) * for more info. * * @default true - * - * @see https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/ */ - selectOnMove?: Ariakit.TabStoreProps[ 'selectOnMove' ]; + selectOnMove?: Ariakit.TabProviderProps[ 'selectOnMove' ]; /** * The id of the tab whose panel is currently visible. * @@ -44,7 +42,7 @@ export type TabsProps = { * in "controlled" mode. When in "controlled" mode, the `null` value will * result in no tabs being selected, and the tablist becoming tabbable. */ - selectedTabId?: Ariakit.TabStoreProps[ 'selectedId' ]; + selectedTabId?: Ariakit.TabProviderProps[ 'selectedId' ]; /** * The id of the tab whose panel is currently visible. * @@ -55,21 +53,22 @@ export type TabsProps = { * Note: this prop will be overridden by the `selectedTabId` prop if it is * provided (meaning the component will be used in "controlled" mode). */ - defaultTabId?: Ariakit.TabStoreProps[ 'defaultSelectedId' ]; + defaultTabId?: Ariakit.TabProviderProps[ 'defaultSelectedId' ]; /** * The function called when the `selectedTabId` changes. */ - onSelect?: Ariakit.TabStoreProps[ 'setSelectedId' ]; + onSelect?: Ariakit.TabProviderProps[ 'setSelectedId' ]; /** * The current active tab `id`. The active tab is the tab element within the * tablist widget that has DOM focus. + * * - `null` represents the tablist (ie. the base composite element). Users * will be able to navigate out of it using arrow keys. * - If `activeTabId` is initially set to `null`, the base composite element * itself will have focus and users will be able to navigate to it using - * arrow keys.activeTabId + * arrow keys. */ - activeTabId?: Ariakit.TabStoreProps[ 'activeId' ]; + activeTabId?: Ariakit.TabProviderProps[ 'activeId' ]; /** * The tab id that should be active by default when the composite widget is * rendered. If `null`, the tablist element itself will have focus @@ -79,21 +78,22 @@ export type TabsProps = { * Note: this prop will be overridden by the `activeTabId` prop if it is * provided. */ - defaultActiveTabId?: Ariakit.TabStoreProps[ 'defaultActiveId' ]; + defaultActiveTabId?: Ariakit.TabProviderProps[ 'defaultActiveId' ]; /** * A callback that gets called when the `activeTabId` state changes. */ - onActiveTabIdChange?: Ariakit.TabStoreProps[ 'setActiveId' ]; + onActiveTabIdChange?: Ariakit.TabProviderProps[ 'setActiveId' ]; /** * Defines the orientation of the tablist and determines which arrow keys * can be used to move focus: + * * - `both`: all arrow keys work. * - `horizontal`: only left and right arrow keys work. * - `vertical`: only up and down arrow keys work. * * @default "horizontal" */ - orientation?: Ariakit.TabStoreProps[ 'orientation' ]; + orientation?: Ariakit.TabProviderProps[ 'orientation' ]; }; export type TabListProps = { @@ -105,7 +105,6 @@ export type TabListProps = { }; // TODO: consider prop name changes (tabId, selectedTabId) -// switch to auto-generated README // compound technique export type TabProps = { diff --git a/packages/components/src/text/hook.ts b/packages/components/src/text/hook.ts index a447b2ce5133be..243b00202460eb 100644 --- a/packages/components/src/text/hook.ts +++ b/packages/components/src/text/hook.ts @@ -105,8 +105,8 @@ export default function useText( getOptimalTextShade( optimizeReadabilityFor ) === 'dark'; sx.optimalTextColor = isOptimalTextColorDark - ? css( { color: COLORS.gray[ 900 ] } ) - : css( { color: COLORS.white } ); + ? css( { color: COLORS.theme.foreground } ) + : css( { color: COLORS.theme.foregroundInverted } ); } return cx( diff --git a/packages/components/src/text/styles.ts b/packages/components/src/text/styles.ts index e777ed4f0941de..7d3b70e2ab2390 100644 --- a/packages/components/src/text/styles.ts +++ b/packages/components/src/text/styles.ts @@ -9,7 +9,7 @@ import { css } from '@emotion/react'; import { COLORS, CONFIG } from '../utils'; export const Text = css` - color: ${ COLORS.gray[ 900 ] }; + color: ${ COLORS.theme.foreground }; line-height: ${ CONFIG.fontLineHeightBase }; margin: 0; text-wrap: balance; /* Fallback for Safari. */ diff --git a/packages/components/src/text/test/__snapshots__/index.tsx.snap b/packages/components/src/text/test/__snapshots__/index.tsx.snap index 1b98c0853ac549..caa876cb24dc78 100644 --- a/packages/components/src/text/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/text/test/__snapshots__/index.tsx.snap @@ -6,7 +6,7 @@ Snapshot Diff: + Base styles @@ -3,8 +3,9 @@ - "color": "#1e1e1e", + "color": "var(--wp-components-color-foreground, #1e1e1e)", "font-size": "calc((13 / 13) * 13px)", "font-weight": "normal", "line-height": "1.4", @@ -19,7 +19,7 @@ Snapshot Diff: exports[`Text should render highlighted words with highlightCaseSensitive 1`] = ` .emotion-0 { - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); line-height: 1.4; margin: 0; text-wrap: balance; @@ -52,7 +52,7 @@ exports[`Text should render highlighted words with highlightCaseSensitive 1`] = exports[`Text snapshot tests should render correctly 1`] = ` .emotion-0 { - color: #1e1e1e; + color: var(--wp-components-color-foreground, #1e1e1e); line-height: 1.4; margin: 0; text-wrap: balance; diff --git a/packages/components/src/text/test/index.tsx b/packages/components/src/text/test/index.tsx index 5fad5582f4d46e..e6f6423b6b572d 100644 --- a/packages/components/src/text/test/index.tsx +++ b/packages/components/src/text/test/index.tsx @@ -25,7 +25,7 @@ describe( 'Text', () => { </Text> ); expect( screen.getByRole( 'heading' ) ).toHaveStyle( { - color: COLORS.white, + color: 'rgb( 255, 255, 255 )', } ); } ); diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index 91e9f291ddf018..18837ae79a325a 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -357,7 +357,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = </div> </div> <button - class="components-button" + class="components-button is-next-40px-default-size" type="button" > Reset @@ -626,7 +626,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options </div> </div> <button - class="components-button" + class="components-button is-next-40px-default-size" type="button" > Reset diff --git a/packages/components/src/toggle-group-control/test/index.tsx b/packages/components/src/toggle-group-control/test/index.tsx index 44cfda69c423cf..28928a9735a378 100644 --- a/packages/components/src/toggle-group-control/test/index.tsx +++ b/packages/components/src/toggle-group-control/test/index.tsx @@ -57,9 +57,15 @@ const ControlledToggleGroupControl = ( { } } value={ value } /> - <Button onClick={ () => setValue( undefined ) }>Reset</Button> + <Button + onClick={ () => setValue( undefined ) } + __next40pxDefaultSize + > + Reset + </Button> { extraButtonOptions?.map( ( obj ) => ( <Button + __next40pxDefaultSize key={ obj.value } onClick={ () => setValue( obj.value ) } > diff --git a/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx b/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx index 0166728dbafba4..56fb5faca56385 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/as-radio-group.tsx @@ -3,7 +3,6 @@ */ import type { ForwardedRef } from 'react'; import * as Ariakit from '@ariakit/react'; -import { useStoreState } from '@ariakit/react'; /** * WordPress dependencies @@ -70,7 +69,7 @@ function UnforwardedToggleGroupControlAsRadioGroup( rtl: isRTL(), } ); - const selectedValue = useStoreState( radio, 'value' ); + const selectedValue = Ariakit.useStoreState( radio, 'value' ); const setValue = radio.setValue; // Ensures that the active id is also reset after the value is "reset" by the consumer. diff --git a/packages/components/src/tooltip/index.tsx b/packages/components/src/tooltip/index.tsx index ce94daf67bfaba..b7184579ceca91 100644 --- a/packages/components/src/tooltip/index.tsx +++ b/packages/components/src/tooltip/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import * as Ariakit from '@ariakit/react'; -import { useStoreState } from '@ariakit/react'; import clsx from 'clsx'; /** @@ -94,7 +93,7 @@ function UnforwardedTooltip( placement: computedPlacement, showTimeout: delay, } ); - const mounted = useStoreState( tooltipStore, 'mounted' ); + const mounted = Ariakit.useStoreState( tooltipStore, 'mounted' ); if ( isNestedInTooltip ) { return isOnlyChild ? ( diff --git a/packages/components/src/tree-grid/stories/index.story.tsx b/packages/components/src/tree-grid/stories/index.story.tsx index f91ba4631d3209..e113103897dbdb 100644 --- a/packages/components/src/tree-grid/stories/index.story.tsx +++ b/packages/components/src/tree-grid/stories/index.story.tsx @@ -111,6 +111,7 @@ const Rows = ( { label="Description" hideLabelFromVision placeholder="Description" + __next40pxDefaultSize { ...props } /> ) } @@ -121,6 +122,7 @@ const Rows = ( { label="Notes" hideLabelFromVision placeholder="Notes" + __next40pxDefaultSize { ...props } /> ) } diff --git a/packages/components/src/tree-select/README.md b/packages/components/src/tree-select/README.md index 3d26488478bd0c..d2f73443a2a880 100644 --- a/packages/components/src/tree-select/README.md +++ b/packages/components/src/tree-select/README.md @@ -1,10 +1,10 @@ # TreeSelect -TreeSelect component is used to generate select input fields. +<!-- This file is generated automatically and cannot be edited directly. Make edits via TypeScript types and TSDocs. --> -## Usage +<p class="callout callout-info">See the <a href="https://wordpress.github.io/gutenberg/?path=/docs/components-treeselect--docs">WordPress Storybook</a> for more detailed, interactive documentation.</p> -Render a user interface to select the parent page in a hierarchy of pages: +Generates a hierarchical select input. ```jsx import { useState } from 'react'; @@ -15,7 +15,8 @@ const MyTreeSelect = () => { return ( <TreeSelect - __nextHasNoMarginBottom + __nextHasNoMarginBottom + __next40pxDefaultSize label="Parent page" noOptionLabel="No parent page" onChange={ ( newPage ) => setPage( newPage ) } @@ -53,48 +54,163 @@ const MyTreeSelect = () => { ## Props -The set of props accepted by the component will be specified below. -Props not included in this set will be applied to the SelectControl component being used. +### `__next40pxDefaultSize` -### label + - Type: `boolean` + - Required: No + - Default: `false` + +Start opting into the larger default height that will become the default size in a future version. + +### `__nextHasNoMarginBottom` + + - Type: `boolean` + - Required: No + - Default: `false` + +Start opting into the new margin-free styles that will become the default in a future version. + +### `children` + + - Type: `ReactNode` + - Required: No + +As an alternative to the `options` prop, `optgroup`s and `options` can be +passed in as `children` for more customizability. + +### `disabled` + + - Type: `boolean` + - Required: No + - Default: `false` + +If true, the `input` will be disabled. + +### `hideLabelFromVision` + + - Type: `boolean` + - Required: No + - Default: `false` + +If true, the label will only be visible to screen readers. + +### `help` + + - Type: `ReactNode` + - Required: No + +Additional description for the control. + +Only use for meaningful description or instructions for the control. An element containing the description will be programmatically associated to the BaseControl by the means of an `aria-describedby` attribute. + +### `label` + + - Type: `ReactNode` + - Required: No If this property is added, a label will be generated using label property as the content. -- Type: `String` -- Required: No +### `labelPosition` + + - Type: `"top" | "bottom" | "side" | "edge"` + - Required: No + - Default: `'top'` + +The position of the label. + +### `noOptionLabel` -### noOptionLabel + - Type: `string` + - Required: No If this property is added, an option will be added with this label to represent empty selection. -- Type: `String` -- Required: No +### `onChange` -### onChange + - Type: `(value: string, extra?: { event?: ChangeEvent<HTMLSelectElement>; }) => void` + - Required: No -A function that receives the id of the new node element that is being selected. +A function that receives the value of the new option that is being selected as input. -- Type: `function` -- Required: Yes +### `options` -### selectedId + - Type: `readonly ({ label: string; value: string; } & Omit<OptionHTMLAttributes<HTMLOptionElement>, "label" | "value">)[]` + - Required: No + +An array of option property objects to be rendered, +each with a `label` and `value` property, as well as any other +`<option>` attributes. + +### `prefix` + + - Type: `ReactNode` + - Required: No + +Renders an element on the left side of the input. + +By default, the prefix is aligned with the edge of the input border, with no padding. +If you want to apply standard padding in accordance with the size variant, wrap the element in +the provided `<InputControlPrefixWrapper>` component. + +```jsx +import { + __experimentalInputControl as InputControl, + __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, +} from '@wordpress/components'; + +<InputControl + prefix={<InputControlPrefixWrapper>@</InputControlPrefixWrapper>} +/> +``` + +### `selectedId` + + - Type: `string` + - Required: No The id of the currently selected node. -- Type: `string` | `string[]` -- Required: No +### `size` -### tree + - Type: `"default" | "small" | "compact" | "__unstable-large"` + - Required: No + - Default: `'default'` -An array containing the tree objects with the possible nodes the user can select. +Adjusts the size of the input. -- Type: `Object[]` -- Required: No +### `suffix` -#### __nextHasNoMarginBottom + - Type: `ReactNode` + - Required: No -Start opting into the new margin-free styles that will become the default in a future version. +Renders an element on the right side of the input. + +By default, the suffix is aligned with the edge of the input border, with no padding. +If you want to apply standard padding in accordance with the size variant, wrap the element in +the provided `<InputControlSuffixWrapper>` component. + +```jsx +import { + __experimentalInputControl as InputControl, + __experimentalInputControlSuffixWrapper as InputControlSuffixWrapper, +} from '@wordpress/components'; + +<InputControl + suffix={<InputControlSuffixWrapper>%</InputControlSuffixWrapper>} +/> +``` + +### `tree` + + - Type: `Tree[]` + - Required: No + +An array containing the tree objects with the possible nodes the user can select. + +### `variant` + + - Type: `"default" | "minimal"` + - Required: No + - Default: `'default'` -- Type: `Boolean` -- Required: No -- Default: `false` +The style variant of the control. diff --git a/packages/components/src/tree-select/docs-manifest.json b/packages/components/src/tree-select/docs-manifest.json new file mode 100644 index 00000000000000..0e74d71d309e10 --- /dev/null +++ b/packages/components/src/tree-select/docs-manifest.json @@ -0,0 +1,5 @@ +{ + "$schema": "../../schemas/docs-manifest.json", + "displayName": "TreeSelect", + "filePath": "./index.tsx" +} diff --git a/packages/components/src/tree-select/index.tsx b/packages/components/src/tree-select/index.tsx index bd92807bff4cc9..66116576361623 100644 --- a/packages/components/src/tree-select/index.tsx +++ b/packages/components/src/tree-select/index.tsx @@ -11,6 +11,7 @@ import { SelectControl } from '../select-control'; import type { TreeSelectProps, Tree, Truthy } from './types'; import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props'; import { ContextSystemProvider } from '../context'; +import { maybeWarnDeprecated36pxSize } from '../utils/deprecated-36px-size'; const CONTEXT_VALUE = { BaseControl: { @@ -35,11 +36,11 @@ function getSelectOptions( } /** - * TreeSelect component is used to generate select input fields. + * Generates a hierarchical select input. * * ```jsx + * import { useState } from 'react'; * import { TreeSelect } from '@wordpress/components'; - * import { useState } from '@wordpress/element'; * * const MyTreeSelect = () => { * const [ page, setPage ] = useState( 'p21' ); @@ -47,6 +48,7 @@ function getSelectOptions( * return ( * <TreeSelect * __nextHasNoMarginBottom + * __next40pxDefaultSize * label="Parent page" * noOptionLabel="No parent page" * onChange={ ( newPage ) => setPage( newPage ) } @@ -99,9 +101,16 @@ export function TreeSelect( props: TreeSelectProps ) { ].filter( < T, >( option: T ): option is Truthy< T > => !! option ); }, [ noOptionLabel, tree ] ); + maybeWarnDeprecated36pxSize( { + componentName: 'TreeSelect', + size: restProps.size, + __next40pxDefaultSize: restProps.__next40pxDefaultSize, + } ); + return ( <ContextSystemProvider value={ CONTEXT_VALUE }> <SelectControl + __shouldNotWarnDeprecated36pxSize { ...{ label, options, onChange } } value={ selectedId } { ...restProps } diff --git a/packages/components/src/tree-select/stories/index.story.tsx b/packages/components/src/tree-select/stories/index.story.tsx index b43245e5e16213..0ef8f44b790db0 100644 --- a/packages/components/src/tree-select/stories/index.story.tsx +++ b/packages/components/src/tree-select/stories/index.story.tsx @@ -50,6 +50,7 @@ const TreeSelectWithState: StoryFn< typeof TreeSelect > = ( props ) => { export const Default = TreeSelectWithState.bind( {} ); Default.args = { __nextHasNoMarginBottom: true, + __next40pxDefaultSize: true, label: 'Label Text', noOptionLabel: 'No parent page', help: 'Help text to explain the select control.', diff --git a/packages/components/src/tree-select/types.ts b/packages/components/src/tree-select/types.ts index da90ece3a658e8..59e8e173fab02f 100644 --- a/packages/components/src/tree-select/types.ts +++ b/packages/components/src/tree-select/types.ts @@ -16,11 +16,18 @@ export interface Tree { // `TreeSelect` inherits props from `SelectControl`, but only // in single selection mode (ie. when the `multiple` prop is not defined). export interface TreeSelectProps - extends Omit< SelectControlSingleSelectionProps, 'value' | 'multiple' > { + extends Omit< + SelectControlSingleSelectionProps, + 'value' | 'multiple' | 'onChange' + > { /** * If this property is added, an option will be added with this label to represent empty selection. */ noOptionLabel?: string; + /** + * A function that receives the value of the new option that is being selected as input. + */ + onChange?: SelectControlSingleSelectionProps[ 'onChange' ]; /** * An array containing the tree objects with the possible nodes the user can select. */ diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index 2033a6f43fede6..09bfef2c53b076 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env", "gutenberg-test-env", @@ -31,7 +29,6 @@ { "path": "../rich-text" }, { "path": "../warning" } ], - "include": [ "src/**/*" ], "exclude": [ "src/**/*.android.js", "src/**/*.ios.js", diff --git a/packages/compose/src/hooks/use-focus-return/index.js b/packages/compose/src/hooks/use-focus-return/index.js index 2cd93b279cd318..36dc7560669652 100644 --- a/packages/compose/src/hooks/use-focus-return/index.js +++ b/packages/compose/src/hooks/use-focus-return/index.js @@ -48,7 +48,13 @@ function useFocusReturn( onFocusReturn ) { return; } - focusedBeforeMount.current = node.ownerDocument.activeElement; + const activeDocument = + node.ownerDocument.activeElement instanceof + window.HTMLIFrameElement + ? node.ownerDocument.activeElement.contentDocument + : node.ownerDocument; + + focusedBeforeMount.current = activeDocument?.activeElement ?? null; } else if ( focusedBeforeMount.current ) { const isFocused = ref.current?.contains( ref.current?.ownerDocument.activeElement diff --git a/packages/core-data/src/fetch/__experimental-fetch-url-data.js b/packages/core-data/src/fetch/__experimental-fetch-url-data.js index effb0566691dfe..003cc0ebf74ebb 100644 --- a/packages/core-data/src/fetch/__experimental-fetch-url-data.js +++ b/packages/core-data/src/fetch/__experimental-fetch-url-data.js @@ -29,7 +29,7 @@ const CACHE = new Map(); * * @async * @param {string} url the URL to request details from. - * @param {Object?} options any options to pass to the underlying fetch. + * @param {?Object} options any options to pass to the underlying fetch. * @example * ```js * import { __experimentalFetchUrlData as fetchUrlData } from '@wordpress/core-data'; diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 0d4a28ad174a19..fb0401509694ef 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -6,7 +6,12 @@ import { createSelector, createRegistrySelector } from '@wordpress/data'; /** * Internal dependencies */ -import { getDefaultTemplateId, getEntityRecord, type State } from './selectors'; +import { + canUser, + getDefaultTemplateId, + getEntityRecord, + type State, +} from './selectors'; import { STORE_NAME } from './name'; import { unlock } from './lock-unlock'; @@ -134,6 +139,13 @@ interface SiteData { export const getHomePage = createRegistrySelector( ( select ) => createSelector( () => { + const canReadSiteData = select( STORE_NAME ).canUser( 'read', { + kind: 'root', + name: 'site', + } ); + if ( ! canReadSiteData ) { + return null; + } const siteData = select( STORE_NAME ).getEntityRecord( 'root', 'site' @@ -156,7 +168,10 @@ export const getHomePage = createRegistrySelector( ( select ) => return { postType: 'wp_template', postId: frontPageTemplateId }; }, ( state ) => [ - getEntityRecord( state, 'root', 'site' ), + canUser( state, 'read', { + kind: 'root', + name: 'site', + } ) && getEntityRecord( state, 'root', 'site' ), getDefaultTemplateId( state, { slug: 'front-page', } ), @@ -165,6 +180,13 @@ export const getHomePage = createRegistrySelector( ( select ) => ); export const getPostsPageId = createRegistrySelector( ( select ) => () => { + const canReadSiteData = select( STORE_NAME ).canUser( 'read', { + kind: 'root', + name: 'site', + } ); + if ( ! canReadSiteData ) { + return null; + } const siteData = select( STORE_NAME ).getEntityRecord( 'root', 'site' ) as | SiteData | undefined; diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index a35403c0493460..4f101035b10130 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -226,7 +226,7 @@ export const getEditedEntityRecord = forwardResolver( 'getEntityRecord' ); * * @param {string} kind Entity kind. * @param {string} name Entity name. - * @param {Object?} query Query Object. If requesting specific fields, fields + * @param {?Object} query Query Object. If requesting specific fields, fields * must always include the ID. */ export const getEntityRecords = diff --git a/packages/core-data/tsconfig.json b/packages/core-data/tsconfig.json index 26602d82ab0c01..57c9d208e4c689 100644 --- a/packages/core-data/tsconfig.json +++ b/packages/core-data/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "checkJs": false, "noImplicitAny": false }, @@ -23,6 +21,5 @@ { "path": "../undo-manager" }, { "path": "../url" }, { "path": "../warning" } - ], - "include": [ "src/**/*" ] + ] } diff --git a/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache b/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache index 48469aa7d0d931..cb7f45b7207946 100644 --- a/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache +++ b/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache @@ -8,8 +8,12 @@ * Description: {{description}} {{/description}} * Version: {{version}} - * Requires at least: 6.6 - * Requires PHP: 7.2 +{{#requiresAtLeast}} + * Requires at least: {{requiresAtLeast}} +{{/requiresAtLeast}} +{{#requiresPHP}} + * Requires PHP: {{requiresPHP}} +{{/requiresPHP}} {{#author}} * Author: {{author}} {{/author}} diff --git a/packages/create-block-interactive-template/plugin-templates/readme.txt.mustache b/packages/create-block-interactive-template/plugin-templates/readme.txt.mustache index c3abf5ae4ec024..19a4c8e78587b6 100644 --- a/packages/create-block-interactive-template/plugin-templates/readme.txt.mustache +++ b/packages/create-block-interactive-template/plugin-templates/readme.txt.mustache @@ -3,7 +3,9 @@ Contributors: {{author}} {{/author}} Tags: block -Tested up to: 6.6 +{{#testedUpTo}} +Tested up to: {{testedUpTo}} +{{/testedUpTo}} Stable tag: {{version}} {{#license}} License: {{license}} diff --git a/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache b/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache index 7ce4be3f7cc739..49959fb5b2f691 100644 --- a/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache +++ b/packages/create-block-tutorial-template/plugin-templates/$slug.php.mustache @@ -8,8 +8,12 @@ * Description: {{description}} {{/description}} * Version: {{version}} - * Requires at least: 6.6 - * Requires PHP: 7.2 +{{#requiresAtLeast}} + * Requires at least: {{requiresAtLeast}} +{{/requiresAtLeast}} +{{#requiresPHP}} + * Requires PHP: {{requiresPHP}} +{{/requiresPHP}} {{#author}} * Author: {{author}} {{/author}} diff --git a/packages/create-block-tutorial-template/plugin-templates/readme.txt.mustache b/packages/create-block-tutorial-template/plugin-templates/readme.txt.mustache index c3abf5ae4ec024..19a4c8e78587b6 100644 --- a/packages/create-block-tutorial-template/plugin-templates/readme.txt.mustache +++ b/packages/create-block-tutorial-template/plugin-templates/readme.txt.mustache @@ -3,7 +3,9 @@ Contributors: {{author}} {{/author}} Tags: block -Tested up to: 6.6 +{{#testedUpTo}} +Tested up to: {{testedUpTo}} +{{/testedUpTo}} Stable tag: {{version}} {{#license}} License: {{license}} diff --git a/packages/create-block/CHANGELOG.md b/packages/create-block/CHANGELOG.md index 73522a9be0726d..9ed6c9f4397067 100644 --- a/packages/create-block/CHANGELOG.md +++ b/packages/create-block/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +### Enhancement + +- Add support for custom `textdomain` property for the scaffolded block ([#57197](https://github.com/WordPress/gutenberg/pull/57197)). +- Allow external templates to customize additional plugin header and readme fields: "Requires at least", "Requires PHP", and "Tested up to" ([#68193](https://github.com/WordPress/gutenberg/pull/68193)) +- Update the default template to scaffold a block in its subfolder to make it easier to update to multiple blocks in a single plugin ([#68175](https://github.com/WordPress/gutenberg/pull/68175)). + +### Internal + +- Refactored the code to use new API introduced together with `@inquirer/prompts` instead of legacy `inquirer` package ([#67877](https://github.com/WordPress/gutenberg/pull/67877)). + ## 4.57.0 (2024-12-11) ### Internal diff --git a/packages/create-block/docs/external-template.md b/packages/create-block/docs/external-template.md index 45c3cba8c9271d..d840896f266f30 100644 --- a/packages/create-block/docs/external-template.md +++ b/packages/create-block/docs/external-template.md @@ -76,10 +76,13 @@ The following configurable variables are used with the template files. Template - `npmDevDependencies` (default: `[]`) – the list of remote npm packages to be installed in the project with [`npm install --save-dev`](https://docs.npmjs.com/cli/v8/commands/npm-install) when `wpScripts` is enabled. - `customPackageJSON` (no default) - allows definition of additional properties for the generated package.json file. -**Plugin header fields** ([learn more](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/)): +**Plugin header and readme fields** (learn more about [header requirements](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) and [readmes](https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/)): - `pluginURI` (no default) – the home page of the plugin. - `version` (default: `'0.1.0'`) – the current version number of the plugin. +- `requiresAtLeast` (default: `'6.7'`) – the lowest WordPress version that the plugin will work on. +- `requiresPHP` (default: `'7.4'`) – the minimum required PHP version for use with this plugin. +- `testedUpTo` (default: `'6.7'`) – the highest WordPress version that the plugin has been tested against. - `author` (default: `'The WordPress Contributors'`) – the name of the plugin author(s). - `license` (default: `'GPL-2.0-or-later'`) – the short name of the plugin’s license. - `licenseURI` (default: `'https://www.gnu.org/licenses/gpl-2.0.html'`) – a link to the full text of the license. @@ -97,6 +100,7 @@ The following configurable variables are used with the template files. Template - `description` (no default) – a short description for your block. - `dashicon` (no default) – an icon property thats makes it easier to identify a block ([available values](https://developer.wordpress.org/resource/dashicons/)). - `category` (default: `'widgets'`) – blocks are grouped into categories to help users browse and discover them. The categories provided by core are `text`, `media`, `design`, `widgets`, `theme`, and `embed`. +- `textdomain` (defaults to the `slug` value) – the text domain used to make strings translatable ([more info](https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#text-domains)). - `attributes` (no default) – block attributes ([more details](https://developer.wordpress.org/block-editor/developers/block-api/block-attributes/)). - `supports` (no default) – optional block extended support features ([more details](https://developer.wordpress.org/block-editor/developers/block-api/block-supports/). - `editorScript` (default: `'file:./index.js'`) – an editor script definition. diff --git a/packages/create-block/lib/check-system-requirements.js b/packages/create-block/lib/check-system-requirements.js index 4a88d167d437c7..152931bc191410 100644 --- a/packages/create-block/lib/check-system-requirements.js +++ b/packages/create-block/lib/check-system-requirements.js @@ -1,7 +1,7 @@ /** * External dependencies */ -const inquirer = require( 'inquirer' ); +const { confirm } = require( '@inquirer/prompts' ); const checkSync = require( 'check-node-version' ); const tools = require( 'check-node-version/tools' ); const { promisify } = require( 'util' ); @@ -34,14 +34,10 @@ async function checkSystemRequirements( engines ) { log.error( 'The program may not complete correctly if you continue.' ); log.info( '' ); - const { yesContinue } = await inquirer.prompt( [ - { - type: 'confirm', - name: 'yesContinue', - message: 'Are you sure you want to continue anyway?', - default: false, - }, - ] ); + const yesContinue = await confirm( { + message: 'Are you sure you want to continue anyway?', + default: false, + } ); if ( ! yesContinue ) { log.error( 'Cancelled.' ); diff --git a/packages/create-block/lib/index.js b/packages/create-block/lib/index.js index da08bcd4ab1dc7..ccc2e91b106e20 100644 --- a/packages/create-block/lib/index.js +++ b/packages/create-block/lib/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -const inquirer = require( 'inquirer' ); +const { confirm, select } = require( '@inquirer/prompts' ); const { capitalCase } = require( 'change-case' ); const program = require( 'commander' ); @@ -14,9 +14,9 @@ const log = require( './log' ); const { engines, version } = require( '../package.json' ); const scaffold = require( './scaffold' ); const { - getPluginTemplate, getDefaultValues, - getPrompts, + getProjectTemplate, + runPrompts, } = require( './templates' ); const commandName = `wp-create-block`; @@ -79,11 +79,13 @@ program targetDir, } ) => { - await checkSystemRequirements( engines ); try { - const pluginTemplate = await getPluginTemplate( templateName ); + await checkSystemRequirements( engines ); + + const projectTemplate = + await getProjectTemplate( templateName ); const availableVariants = Object.keys( - pluginTemplate.variants + projectTemplate.variants ); if ( variant && ! availableVariants.includes( variant ) ) { if ( ! availableVariants.length ) { @@ -113,7 +115,7 @@ program if ( slug ) { const defaultValues = getDefaultValues( - pluginTemplate, + projectTemplate, variant ); const answers = { @@ -123,7 +125,7 @@ program title: capitalCase( slug ), ...optionsValues, }; - await scaffold( pluginTemplate, answers ); + await scaffold( projectTemplate, answers ); } else { log.info( '' ); log.info( @@ -133,25 +135,22 @@ program ); if ( ! variant && availableVariants.length > 1 ) { - const result = await inquirer.prompt( { - type: 'list', - name: 'variant', + variant = await select( { message: 'The template variant to use for this block:', - choices: availableVariants, + choices: availableVariants.map( ( value ) => ( { + value, + } ) ), } ); - variant = result.variant; } const defaultValues = getDefaultValues( - pluginTemplate, + projectTemplate, variant ); - const filterOptionsProvided = ( { name } ) => - ! Object.keys( optionsValues ).includes( name ); - const blockPrompts = getPrompts( - pluginTemplate, + const blockAnswers = await runPrompts( + projectTemplate, [ 'slug', 'namespace', @@ -159,45 +158,36 @@ program 'description', 'dashicon', 'category', - ], - variant - ).filter( filterOptionsProvided ); - const blockAnswers = await inquirer.prompt( blockPrompts ); - - const pluginAnswers = plugin - ? await inquirer - .prompt( { - type: 'confirm', - name: 'configurePlugin', - message: - 'Do you want to customize the WordPress plugin?', - default: false, - } ) - .then( async ( { configurePlugin } ) => { - if ( ! configurePlugin ) { - return {}; - } + ! plugin && 'textdomain', + ].filter( Boolean ), + variant, + optionsValues + ); - const pluginPrompts = getPrompts( - pluginTemplate, - [ - 'pluginURI', - 'version', - 'author', - 'license', - 'licenseURI', - 'domainPath', - 'updateURI', - ], - variant - ).filter( filterOptionsProvided ); - const result = - await inquirer.prompt( pluginPrompts ); - return result; - } ) - : {}; + const pluginAnswers = + plugin && + ( await confirm( { + message: + 'Do you want to customize the WordPress plugin?', + default: false, + } ) ) + ? await runPrompts( + projectTemplate, + [ + 'pluginURI', + 'version', + 'author', + 'license', + 'licenseURI', + 'domainPath', + 'updateURI', + ], + variant, + optionsValues + ) + : {}; - await scaffold( pluginTemplate, { + await scaffold( projectTemplate, { ...defaultValues, ...optionsValues, variant, @@ -209,6 +199,9 @@ program if ( error instanceof CLIError ) { log.error( error.message ); process.exit( 1 ); + } else if ( error.name === 'ExitPromptError' ) { + log.info( 'Cancelled.' ); + process.exit( 1 ); } else { throw error; } diff --git a/packages/create-block/lib/prompts.js b/packages/create-block/lib/prompts.js index 12da9f892b80e6..88bdaf22635d36 100644 --- a/packages/create-block/lib/prompts.js +++ b/packages/create-block/lib/prompts.js @@ -11,7 +11,6 @@ const upperFirst = ( [ firstLetter, ...rest ] ) => // Block metadata. const slug = { type: 'input', - name: 'slug', message: 'The block slug used for identification (also the output folder name):', validate( input ) { @@ -25,7 +24,6 @@ const slug = { const namespace = { type: 'input', - name: 'namespace', message: 'The internal namespace for the block name (something unique for your products):', validate( input ) { @@ -39,25 +37,22 @@ const namespace = { const title = { type: 'input', - name: 'title', message: 'The display title for your block:', - filter( input ) { + transformer( input ) { return input && upperFirst( input ); }, }; const description = { type: 'input', - name: 'description', message: 'The short description for your block (optional):', - filter( input ) { + transformer( input ) { return input && upperFirst( input ); }, }; const dashicon = { type: 'input', - name: 'dashicon', message: 'The dashicon to make it easier to identify your block (optional):', validate( input ) { @@ -67,29 +62,41 @@ const dashicon = { return true; }, - filter( input ) { + transformer( input ) { return input && input.replace( /dashicon(s)?-/, '' ); }, }; const category = { - type: 'list', - name: 'category', + type: 'select', message: 'The category name to help users browse and discover your block:', - choices: [ 'text', 'media', 'design', 'widgets', 'theme', 'embed' ], + choices: [ 'text', 'media', 'design', 'widgets', 'theme', 'embed' ].map( + ( value ) => ( { value } ) + ), +}; + +const textdomain = { + type: 'input', + message: + 'The text domain used to make strings translatable in the block (optional):', + validate( input ) { + if ( input.length && ! /^[a-z][a-z0-9\-]*$/.test( input ) ) { + return 'Invalid text domain specified. Text domain can contain only lowercase alphanumeric characters or dashes, and start with a letter.'; + } + + return true; + }, }; // Plugin header fields. const pluginURI = { type: 'input', - name: 'pluginURI', message: 'The home page of the plugin (optional). Unique URL outside of WordPress.org:', }; const version = { type: 'input', - name: 'version', message: 'The current version number of the plugin:', validate( input ) { // Regular expression was copied from https://semver.org. @@ -105,32 +112,27 @@ const version = { const author = { type: 'input', - name: 'author', message: 'The name of the plugin author (optional). Multiple authors may be listed using commas:', }; const license = { type: 'input', - name: 'license', message: 'The short name of the plugin’s license (optional):', }; const licenseURI = { type: 'input', - name: 'licenseURI', message: 'A link to the full text of the license (optional):', }; const domainPath = { type: 'input', - name: 'domainPath', message: 'A custom domain path for the translations (optional):', }; const updateURI = { type: 'input', - name: 'updateURI', message: 'A custom update URI for the plugin (optional):', }; @@ -141,6 +143,7 @@ module.exports = { description, dashicon, category, + textdomain, pluginURI, version, author, diff --git a/packages/create-block/lib/scaffold.js b/packages/create-block/lib/scaffold.js index 73b9f549908867..44812e3d5954d6 100644 --- a/packages/create-block/lib/scaffold.js +++ b/packages/create-block/lib/scaffold.js @@ -26,6 +26,7 @@ module.exports = async ( description, dashicon, category, + textdomain, attributes, supports, author, @@ -35,6 +36,9 @@ module.exports = async ( domainPath, updateURI, version, + requiresAtLeast, + requiresPHP, + testedUpTo, wpScripts, wpEnv, npmDependencies, @@ -57,13 +61,12 @@ module.exports = async ( } ) => { slug = slug.toLowerCase(); - namespace = namespace.toLowerCase(); const rootDirectory = join( process.cwd(), targetDir || slug ); const transformedValues = transformer( { $schema, apiVersion, plugin, - namespace, + namespace: namespace.toLowerCase(), slug, title, description, @@ -78,12 +81,15 @@ module.exports = async ( domainPath, updateURI, version, + requiresAtLeast, + requiresPHP, + testedUpTo, wpScripts, wpEnv, npmDependencies, npmDevDependencies, customScripts, - folderName, + folderName: folderName.replace( /\$slug/g, slug ), editorScript, editorStyle, style, @@ -95,7 +101,7 @@ module.exports = async ( customPackageJSON, customBlockJSON, example, - textdomain: slug, + textdomain: textdomain || slug, rootDirectory, } ); diff --git a/packages/create-block/lib/templates.js b/packages/create-block/lib/templates.js index 4e70ee66fd3a40..3604ac99a35eac 100644 --- a/packages/create-block/lib/templates.js +++ b/packages/create-block/lib/templates.js @@ -1,6 +1,7 @@ /** * External dependencies */ +const inquirer = require( '@inquirer/prompts' ); const { command } = require( 'execa' ); const glob = require( 'fast-glob' ); const { resolve } = require( 'path' ); @@ -58,6 +59,7 @@ const predefinedPluginTemplates = { }, viewScript: 'file:./view.js', example: {}, + folderName: './src/$slug', }, variants: { static: {}, @@ -157,7 +159,7 @@ const configToTemplate = async ( { }; }; -const getPluginTemplate = async ( templateName ) => { +const getProjectTemplate = async ( templateName ) => { if ( predefinedPluginTemplates[ templateName ] ) { return await configToTemplate( predefinedPluginTemplates[ templateName ] @@ -224,16 +226,20 @@ const getPluginTemplate = async ( templateName ) => { } }; -const getDefaultValues = ( pluginTemplate, variant ) => { +const getDefaultValues = ( projectTemplate, variant ) => { return { $schema: 'https://schemas.wp.org/trunk/block.json', apiVersion: 3, namespace: 'create-block', category: 'widgets', + textdomain: '', author: 'The WordPress Contributors', license: 'GPL-2.0-or-later', licenseURI: 'https://www.gnu.org/licenses/gpl-2.0.html', version: '0.1.0', + requiresAtLeast: '6.7', + requiresPHP: '7.4', + testedUpTo: '6.7', wpScripts: true, customScripts: {}, wpEnv: false, @@ -243,20 +249,33 @@ const getDefaultValues = ( pluginTemplate, variant ) => { editorStyle: 'file:./index.css', style: 'file:./style-index.css', transformer: ( view ) => view, - ...pluginTemplate.defaultValues, - ...pluginTemplate.variants?.[ variant ], - variantVars: getVariantVars( pluginTemplate.variants, variant ), + ...projectTemplate.defaultValues, + ...projectTemplate.variants?.[ variant ], + variantVars: getVariantVars( projectTemplate.variants, variant ), }; }; -const getPrompts = ( pluginTemplate, keys, variant ) => { - const defaultValues = getDefaultValues( pluginTemplate, variant ); - return keys.map( ( promptName ) => { - return { - ...prompts[ promptName ], +const runPrompts = async ( + projectTemplate, + promptNames, + variant, + optionsValues +) => { + const defaultValues = getDefaultValues( projectTemplate, variant ); + const result = {}; + for ( const promptName of promptNames ) { + if ( Object.keys( optionsValues ).includes( promptName ) ) { + continue; + } + + const { type, ...config } = prompts[ promptName ]; + result[ promptName ] = await inquirer[ type ]( { + ...config, default: defaultValues[ promptName ], - }; - } ); + } ); + } + + return result; }; const getVariantVars = ( variants, variant ) => { @@ -277,7 +296,7 @@ const getVariantVars = ( variants, variant ) => { }; module.exports = { - getPluginTemplate, getDefaultValues, - getPrompts, + getProjectTemplate, + runPrompts, }; diff --git a/packages/create-block/lib/templates/es5/$slug.php.mustache b/packages/create-block/lib/templates/es5/$slug.php.mustache index 825fd1bfd8b5aa..5beb2ca06712c9 100644 --- a/packages/create-block/lib/templates/es5/$slug.php.mustache +++ b/packages/create-block/lib/templates/es5/$slug.php.mustache @@ -7,9 +7,13 @@ {{#description}} * Description: {{description}} {{/description}} - * Requires at least: 6.6 - * Requires PHP: 7.2 * Version: {{version}} +{{#requiresAtLeast}} + * Requires at least: {{requiresAtLeast}} +{{/requiresAtLeast}} +{{#requiresPHP}} + * Requires PHP: {{requiresPHP}} +{{/requiresPHP}} {{#author}} * Author: {{author}} {{/author}} diff --git a/packages/create-block/lib/templates/es5/readme.txt.mustache b/packages/create-block/lib/templates/es5/readme.txt.mustache index c3abf5ae4ec024..19a4c8e78587b6 100644 --- a/packages/create-block/lib/templates/es5/readme.txt.mustache +++ b/packages/create-block/lib/templates/es5/readme.txt.mustache @@ -3,7 +3,9 @@ Contributors: {{author}} {{/author}} Tags: block -Tested up to: 6.6 +{{#testedUpTo}} +Tested up to: {{testedUpTo}} +{{/testedUpTo}} Stable tag: {{version}} {{#license}} License: {{license}} diff --git a/packages/create-block/lib/templates/plugin/$slug.php.mustache b/packages/create-block/lib/templates/plugin/$slug.php.mustache index 75666af3a850b2..e229560a655c36 100644 --- a/packages/create-block/lib/templates/plugin/$slug.php.mustache +++ b/packages/create-block/lib/templates/plugin/$slug.php.mustache @@ -7,9 +7,13 @@ {{#description}} * Description: {{description}} {{/description}} - * Requires at least: 6.6 - * Requires PHP: 7.2 * Version: {{version}} +{{#requiresAtLeast}} + * Requires at least: {{requiresAtLeast}} +{{/requiresAtLeast}} +{{#requiresPHP}} + * Requires PHP: {{requiresPHP}} +{{/requiresPHP}} {{#author}} * Author: {{author}} {{/author}} @@ -42,6 +46,6 @@ if ( ! defined( 'ABSPATH' ) ) { * @see https://developer.wordpress.org/reference/functions/register_block_type/ */ function {{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init() { - register_block_type( __DIR__ . '/build' ); + register_block_type( __DIR__ . '/build/{{slug}}' ); } add_action( 'init', '{{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init' ); diff --git a/packages/create-block/lib/templates/plugin/readme.txt.mustache b/packages/create-block/lib/templates/plugin/readme.txt.mustache index c3abf5ae4ec024..19a4c8e78587b6 100644 --- a/packages/create-block/lib/templates/plugin/readme.txt.mustache +++ b/packages/create-block/lib/templates/plugin/readme.txt.mustache @@ -3,7 +3,9 @@ Contributors: {{author}} {{/author}} Tags: block -Tested up to: 6.6 +{{#testedUpTo}} +Tested up to: {{testedUpTo}} +{{/testedUpTo}} Stable tag: {{version}} {{#license}} License: {{license}} diff --git a/packages/create-block/package.json b/packages/create-block/package.json index 375fee43ba1f73..728cf04b3f4437 100644 --- a/packages/create-block/package.json +++ b/packages/create-block/package.json @@ -31,6 +31,7 @@ "wp-create-block": "./index.js" }, "dependencies": { + "@inquirer/prompts": "^7.2.0", "@wordpress/lazy-import": "*", "chalk": "^4.0.0", "change-case": "^4.1.2", @@ -38,7 +39,6 @@ "commander": "^9.2.0", "execa": "^4.0.2", "fast-glob": "^3.2.7", - "inquirer": "^7.1.0", "make-dir": "^3.0.0", "mustache": "^4.0.0", "npm-package-arg": "^8.1.5", diff --git a/packages/customize-widgets/src/components/header/style.scss b/packages/customize-widgets/src/components/header/style.scss index 5c3f37a0bf0d42..73789282108af6 100644 --- a/packages/customize-widgets/src/components/header/style.scss +++ b/packages/customize-widgets/src/components/header/style.scss @@ -33,16 +33,25 @@ border-radius: $radius-small; color: $white; padding: 0; - min-width: $grid-unit-30; - height: $grid-unit-30; + min-width: $grid-unit-40; + height: $grid-unit-40; margin: $grid-unit-15 0 $grid-unit-15 auto; &::before { content: none; } + svg { + transition: transform cubic-bezier(0.165, 0.84, 0.44, 1) 0.2s; + @include reduce-motion("transition"); + } + &.is-pressed { background: $gray-900; + + svg { + transform: rotate(45deg); + } } } diff --git a/packages/data-controls/tsconfig.json b/packages/data-controls/tsconfig.json index 5ccc6045880d4a..faa13b152672b6 100644 --- a/packages/data-controls/tsconfig.json +++ b/packages/data-controls/tsconfig.json @@ -2,14 +2,11 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ] }, "references": [ { "path": "../api-fetch" }, { "path": "../data" }, { "path": "../deprecated" } - ], - "include": [ "src/**/*" ] + ] } diff --git a/packages/data/README.md b/packages/data/README.md index 67c01af24bde32..00105722bd04fb 100644 --- a/packages/data/README.md +++ b/packages/data/README.md @@ -392,7 +392,7 @@ Creates a new store registry, given an optional object of initial store configur _Parameters_ - _storeConfigs_ `Object`: Initial store configurations. -- _parent_ `Object?`: Parent registry. +- _parent_ `?Object`: Parent registry. _Returns_ diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 3e7a8fdd8b5a07..8db8bfbbbb702d 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -49,7 +49,7 @@ function getStoreName( storeNameOrDescriptor ) { * configurations. * * @param {Object} storeConfigs Initial store configurations. - * @param {Object?} parent Parent registry. + * @param {?Object} parent Parent registry. * * @return {WPDataRegistry} Data registry. */ diff --git a/packages/data/tsconfig.json b/packages/data/tsconfig.json index 2bfc881dc6216e..b73eca0d342f04 100644 --- a/packages/data/tsconfig.json +++ b/packages/data/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "checkJs": false }, "references": [ @@ -14,6 +12,5 @@ { "path": "../is-shallow-equal" }, { "path": "../priority-queue" }, { "path": "../redux-routine" } - ], - "include": [ "src/**/*" ] + ] } diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 0468a277ba292e..965d98e80d6aea 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -2,24 +2,32 @@ ## Unreleased +### Bug Fixes + +- Fixed commonjs export ([#67962](https://github.com/WordPress/gutenberg/pull/67962)) + +### Features + +- Add support for hierarchical visualization of data. `DataViews` gets a new prop `getItemLevel` that should return the hierarchical level of the item. The view can use `view.showLevels` to display the levels. It's up to the consumer data source to prepare this information. + ## 4.10.0 (2024-12-11) -## Breaking Changes +### Breaking Changes - Support showing or hiding title, media and description fields ([#67477](https://github.com/WordPress/gutenberg/pull/67477)). -- Unify the `title`, `media` and `description` fields for the different layouts. So instead of the previous `view.layout.mediaField`, `view.layout.primaryField` and `view.layout.columnFields`, all the layouts now support these three fields with the following config ([#67477](https://github.com/WordPress/gutenberg/pull/67477)): +- Unify the `title`, `media` and `description` fields for the different layouts. So instead of the previous `view.layout.mediaField`, `view.layout.primaryField` and `view.layout.columnFields`, all the layouts now support these three fields with the following config ([#67477](https://github.com/WordPress/gutenberg/pull/67477)): ```js const view = { - type: 'table', - titleField: 'title', - mediaField: 'media', - descriptionField: 'description', - fields: [ 'author', 'date' ], -} + type: 'table', + titleField: 'title', + mediaField: 'media', + descriptionField: 'description', + fields: [ 'author', 'date' ], +}; ``` -## Internal +### Internal - Upgraded `@ariakit/react` (v0.4.13) and `@ariakit/test` (v0.4.5) ([#65907](https://github.com/WordPress/gutenberg/pull/65907)). - Upgraded `@ariakit/react` (v0.4.15) and `@ariakit/test` (v0.4.7) ([#67404](https://github.com/WordPress/gutenberg/pull/67404)). diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 6f74a13d8f197a..4cce66a6ae6b26 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -2,8 +2,8 @@ The DataViews package offers two React components and a few utilities to work with a list of data: -- `DataViews`: to render the dataset using different types of layouts (table, grid, list) and interaction capabilities (search, filters, sorting, etc.). -- `DataForm`: to edit the items of the dataset. +- `DataViews`: to render the dataset using different types of layouts (table, grid, list) and interaction capabilities (search, filters, sorting, etc.). +- `DataForm`: to edit the items of the dataset. ## Installation @@ -23,13 +23,15 @@ npm install @wordpress/dataviews --save The `DataViews` component receives data and some other configuration to render the dataset. It'll call the `onChangeView` callback every time the user has interacted with the dataset in some way (sorted, filtered, changed layout, etc.): -![DataViews flow](https://developer.wordpress.org/files/2024/09/368600071-20aa078f-7c3d-406d-8dd0-8b764addd22a.png "DataViews flow") +![DataViews flow](https://developer.wordpress.org/files/2024/09/368600071-20aa078f-7c3d-406d-8dd0-8b764addd22a.png 'DataViews flow') Example: ```jsx const Example = () => { - const onChangeView = () => { /* React to user changes. */ } + const onChangeView = () => { + /* React to user changes. */ + }; return ( <DataViews @@ -45,7 +47,6 @@ const Example = () => { }; ``` - ### Properties #### `data`: `Object[]` @@ -87,6 +88,19 @@ Example: } ``` +#### `getItemLevel`: `function` + +A function that receives an item and returns its hierarchical level. It's optional, but this property must be passed for DataViews to display the hierarchical levels of the data if `view.showLevels` is true. + +Example: + +```js +// Example implementation +{ + getItemLevel={ ( item ) => item.level } +} +``` + #### `fields`: `Object[]` The fields describe the visible items for each record in the dataset and how they behave (how to sort them, display them, etc.). See "Fields API" for a description of every property. @@ -185,21 +199,23 @@ Properties: - `field`: the field used for sorting the dataset. - `direction`: the direction to use for sorting, one of `asc` or `desc`. + - `titleField`: The id of the field representing the title of the record. - `mediaField`: The id of the field representing the media of the record. - `descriptionField`: The id of the field representing the description of the record. - `showTitle`: Whether the title should be shown in the UI. `true` by default. - `showMedia`: Whether the media should be shown in the UI. `true` by default. - `showDescription`: Whether the description should be shown in the UI. `true` by default. +- `showLevels`: Whether to display the hierarchical levels for the data. `false` by default. See related `getItemLevel` DataView prop. - `fields`: a list of remaining field `id` that are visible in the UI and the specific order in which they are displayed. - `layout`: config that is specific to a particular layout type. ##### Properties of `layout` -| Properties of `layout` | Table | Grid | List | -| --------------------------------------------------------------------------------------------------------------- | ----- | ---- | ---- | -| `badgeFields`: a list of field's `id` to render without label and styled as badges. | | ✓ | | -| `styles`: additional `width`, `maxWidth`, `minWidth` styles for each field column. | ✓ | | | +| Properties of `layout` | Table | Grid | List | +| ----------------------------------------------------------------------------------- | ----- | ---- | ---- | +| `badgeFields`: a list of field's `id` to render without label and styled as badges. | | ✓ | | +| `styles`: additional `width`, `maxWidth`, `minWidth` styles for each field column. | ✓ | | | #### `onChangeView`: `function` @@ -302,8 +318,8 @@ const actions = [ RenderModal: ( { items, closeModal, onActionPerformed } ) => ( <div> <p>Are you sure you want to delete { items.length } item(s)?</p> - <Button - variant="primary" + <Button + variant="primary" onClick={() => { console.log( 'Deleting items:', items ); onActionPerformed(); @@ -348,7 +364,7 @@ const defaultLayouts = { }, grid: { showMedia: true, - } + }, }; ``` @@ -366,11 +382,11 @@ Callback that signals the user selected one of more items. It receives the list If `selection` and `onChangeSelection` are provided, the `DataViews` component behaves like a controlled component. Otherwise, it behaves like an uncontrolled component. -### `isItemClickable`: `function` +#### `isItemClickable`: `function` A function that determines if a media field or a primary field is clickable. It receives an item as an argument and returns a boolean value indicating whether the item can be clicked. -### `onClickItem`: `function` +#### `onClickItem`: `function` A callback function that is triggered when a user clicks on a media field or primary field. This function is currently implemented only in the `grid` and `list` views. @@ -395,8 +411,8 @@ const Example = () => { form={ form } onChange={ onChange } /> - ) -} + ); +}; ``` ### Properties @@ -439,8 +455,30 @@ const fields = [ #### `form`: `Object[]` -- `type`: either `regular` or `panel`. -- `fields`: a list of fields ids that should be rendered. +- `type`: either `regular` or `panel`. +- `labelPosition`: either `side`, `top`, or `none`. +- `fields`: a list of fields ids that should be rendered. Field ids can also be defined as an object and allow you to define a `layout`, `labelPosition` or `children` if displaying combined fields. See "Form Field API" for a description of every property. + +Example: + +```js +const form = { + type: 'panel', + fields: [ + 'title', + 'data', + { + id: 'status', + label: 'Status & Visibility', + children: [ 'status', 'password' ], + }, + { + id: 'featured_media', + layout: 'regular', + }, + ], +}; +``` #### `onChange`: `function` @@ -471,10 +509,10 @@ const onChange = ( edits ) => { return ( <DataForm - data={data} - fields={fields} - form={form} - onChange={onChange} + data={ data } + fields={ fields } + form={ form } + onChange={ onChange } /> ); ``` @@ -487,16 +525,16 @@ Utility to apply the view config (filters, search, sorting, and pagination) to a Parameters: -- `data`: the dataset, as described in the "data" property of DataViews. -- `view`: the view config, as described in the "view" property of DataViews. -- `fields`: the fields config, as described in the "fields" property of DataViews. +- `data`: the dataset, as described in the "data" property of DataViews. +- `view`: the view config, as described in the "view" property of DataViews. +- `fields`: the fields config, as described in the "fields" property of DataViews. Returns an object containing: -- `data`: the new dataset, with the view config applied. -- `paginationInfo`: object containing the following properties: - - `totalItems`: total number of items for the current view config. - - `totalPages`: total number of pages for the current view config. +- `data`: the new dataset, with the view config applied. +- `paginationInfo`: object containing the following properties: + - `totalItems`: total number of items for the current view config. + - `totalPages`: total number of pages for the current view config. ### `isItemValid` @@ -504,9 +542,9 @@ Utility is used to determine whether or not the given item's value is valid acco Parameters: -- `item`: the item, as described in the "data" property of DataForm. -- `fields`: the fields config, as described in the "fields" property of DataForm. -- `form`: the form config, as described in the "form" property of DataForm. +- `item`: the item, as described in the "data" property of DataForm. +- `fields`: the fields config, as described in the "fields" property of DataForm. +- `form`: the form config, as described in the "form" property of DataForm. Returns a boolean indicating if the item is valid (true) or not (false). @@ -516,17 +554,17 @@ Returns a boolean indicating if the item is valid (true) or not (false). The unique identifier of the action. -- Type: `string` -- Required -- Example: `move-to-trash` +- Type: `string` +- Required +- Example: `move-to-trash` -### `label` +### `label` The user facing description of the action. -- Type: `string | function` -- Required -- Example: +- Type: `string | function` +- Required +- Example: ```js { @@ -538,7 +576,7 @@ or ```js { - label: ( items ) => items.length > 1 ? 'Delete items' : 'Delete item' + label: ( items ) => ( items.length > 1 ? 'Delete items' : 'Delete item' ); } ``` @@ -546,27 +584,27 @@ or Whether the action should be displayed inline (primary) or only displayed in the "More actions" menu (secondary). -- Type: `boolean` -- Optional +- Type: `boolean` +- Optional ### `icon` Icon to show for primary actions. -- Type: SVG element -- Required for primary actions, optional for secondary actions. +- Type: SVG element +- Required for primary actions, optional for secondary actions. ### `isEligible` Function that determines whether the action can be performed for a given record. -- Type: `function` -- Optional. If not present, action is considered eligible for all items. -- Example: +- Type: `function` +- Optional. If not present, action is considered eligible for all items. +- Example: ```js { - isEligible: ( item ) => item.status === 'published' + isEligible: ( item ) => item.status === 'published'; } ``` @@ -574,47 +612,47 @@ Function that determines whether the action can be performed for a given record. Whether the action can delete data, in which case the UI communicates it via a red color. -- Type: `boolean` -- Optional +- Type: `boolean` +- Optional ### `supportsBulk` Whether the action can operate over multiple items at once. -- Type: `boolean` -- Optional -- Default: `false` +- Type: `boolean` +- Optional +- Default: `false` ### `disabled` Whether the action is disabled. -- Type: `boolean` -- Optional -- Default: `false` +- Type: `boolean` +- Optional +- Default: `false` ### `context` Where this action would be visible. -- Type: `string` -- Optional -- One of: `list`, `single` +- Type: `string` +- Optional +- One of: `list`, `single` ### `callback` Function that performs the required action. -- Type: `function` -- Either `callback` or `RenderModal` must be provided. If `RenderModal` is provided, `callback` will be ignored -- Example: +- Type: `function` +- Either `callback` or `RenderModal` must be provided. If `RenderModal` is provided, `callback` will be ignored +- Example: ```js { callback: ( items, { onActionPerformed } ) => { // Perform action. onActionPerformed?.( items ); - } + }; } ``` @@ -622,9 +660,9 @@ Function that performs the required action. Component to render UI in a modal for the action. -- Type: `ReactElement` -- Either `callback` or `RenderModal` must be provided. If `RenderModal` is provided, `callback` will be ignored. -- Example: +- Type: `ReactElement` +- Either `callback` or `RenderModal` must be provided. If `RenderModal` is provided, `callback` will be ignored. +- Example: ```jsx { @@ -648,7 +686,7 @@ Component to render UI in a modal for the action. </HStack> </form> ); - } + }; } ``` @@ -656,17 +694,16 @@ Component to render UI in a modal for the action. Controls visibility of the modal's header when using `RenderModal`. -- Type: `boolean` -- Optional -- When false and using `RenderModal`, the action's label is used in modal header +- Type: `boolean` +- Optional +- When false and using `RenderModal`, the action's label is used in modal header ### `modalHeader` The header text to show in the modal. -- Type: `string` -- Optional - +- Type: `string` +- Optional ## Fields API @@ -674,13 +711,15 @@ The header text to show in the modal. The unique identifier of the field. -- Type: `string`. -- Required. +- Type: `string`. +- Required. Example: ```js -{ id: 'field_id' } +{ + id: 'field_id'; +} ``` ### `type` @@ -689,44 +728,50 @@ Field type. One of `text`, `integer`, `datetime`. If a field declares a `type`, it gets default implementations for the `sort`, `isValid`, and `Edit` functions if no other values are specified. -- Type: `string`. -- Optional. +- Type: `string`. +- Optional. Example: ```js -{ type: 'text' } +{ + type: 'text'; +} ``` ### `label` The field's name. This will be used across the UI. -- Type: `string`. -- Optional. -- Defaults to the `id` value. +- Type: `string`. +- Optional. +- Defaults to the `id` value. Example: ```js -{ label: 'Title' } +{ + label: 'Title'; +} ``` ### `header` React component used by the layouts to display the field name — useful to add icons, etc. It's complementary to the `label` property. -- Type: React component. -- Optional. -- Defaults to the `label` value. -- Props: none. -- Returns a React element that represents the field's name. +- Type: React component. +- Optional. +- Defaults to the `label` value. +- Props: none. +- Returns a React element that represents the field's name. Example: ```js { - header: () => { /* Returns a react element. */ } + header: () => { + /* Returns a react element. */ + }; } ``` @@ -734,18 +779,20 @@ Example: React component that returns the value of a field. This value is used to sort or filter the fields. -- Type: React component. -- Optional. -- Defaults to `item[ id ]`. -- Props: - - `item` value to be processed. -- Returns a value that represents the field. +- Type: React component. +- Optional. +- Defaults to `item[ id ]`. +- Props: + - `item` value to be processed. +- Returns a value that represents the field. Example: ```js { - getValue: ( { item } ) => { /* The field's value. */ }; + getValue: ( { item } ) => { + /* The field's value. */ + }; } ``` @@ -753,18 +800,20 @@ Example: React component that renders the field. This is used by the layouts. -- Type: React component. -- Optional. -- Defaults to `getValue`. -- Props - - `item` value to be processed. -- Returns a React element that represents the field's value. +- Type: React component. +- Optional. +- Defaults to `getValue`. +- Props + - `item` value to be processed. +- Returns a React element that represents the field's value. Example: ```js { - render: ( { item} ) => { /* React element to be displayed. */ } + render: ( { item } ) => { + /* React element to be displayed. */ + }; } ``` @@ -772,26 +821,21 @@ Example: React component that renders the control to edit the field. -- Type: React component | `string`. If it's a string, it needs to be one of `text`, `integer`, `datetime`, `radio`, `select`. -- Required by DataForm. Optional if the field provided a `type`. -- Props: - - `data`: the item to be processed - - `field`: the field definition - - `onChange`: the callback with the updates - - `hideLabelFromVision`: boolean representing if the label should be hidden -- Returns a React element to edit the field's value. +- Type: React component | `string`. If it's a string, it needs to be one of `text`, `integer`, `datetime`, `radio`, `select`. +- Required by DataForm. Optional if the field provided a `type`. +- Props: + - `data`: the item to be processed + - `field`: the field definition + - `onChange`: the callback with the updates + - `hideLabelFromVision`: boolean representing if the label should be hidden +- Returns a React element to edit the field's value. Example: ```js // A custom control defined by the field. { - Edit: ( { - data, - field, - onChange, - hideLabelFromVision - } ) => { + Edit: ( { data, field, onChange, hideLabelFromVision } ) => { const value = field.getValue( { item: data } ); return ( @@ -801,14 +845,14 @@ Example: hideLabelFromVision /> ); - } + }; } ``` ```js // Use one of the core controls. { - Edit: 'radio' + Edit: 'radio'; } ``` @@ -816,7 +860,7 @@ Example: // Edit is optional when field's type is present. // The field will use the default Edit function for text. { - type: 'text' + type: 'text'; } ``` @@ -833,16 +877,16 @@ Example: Function to sort the records. -- Type: `function`. -- Optional. -- Args - - `a`: the first item to compare - - `b`: the second item to compare - - `direction`: either `asc` (ascending) or `desc` (descending) -- Returns a number where: - - a negative value indicates that `a` should come before `b` - - a positive value indicates that `a` should come after `b` - - 0 indicates that `a` and `b` are considered equal +- Type: `function`. +- Optional. +- Args + - `a`: the first item to compare + - `b`: the second item to compare + - `direction`: either `asc` (ascending) or `desc` (descending) +- Returns a number where: + - a negative value indicates that `a` should come before `b` + - a positive value indicates that `a` should come after `b` + - 0 indicates that `a` and `b` are considered equal Example: @@ -853,7 +897,7 @@ Example: return direction === 'asc' ? a.localeCompare( b ) : b.localeCompare( a ); - } + }; } ``` @@ -861,7 +905,7 @@ Example: // If field type is provided, // the field gets a default sort function. { - type: 'number' + type: 'number'; } ``` @@ -869,8 +913,10 @@ Example: // Even if a field type is provided, // fields can override the default sort function assigned for that type. { - type: 'number' - sort: ( a, b, direction ) => { /* Custom sort */ } + type: 'number'; + sort: ( a, b, direction ) => { + /* Custom sort */ + }; } ``` @@ -878,13 +924,13 @@ Example: Function to validate a field's value. -- Type: function. -- Optional. -- Args - - `item`: the data to validate - - `context`: an object containing the following props: - - `elements`: the elements defined by the field -- Returns a boolean, indicating if the field is valid or not. +- Type: function. +- Optional. +- Args + - `item`: the data to validate + - `context`: an object containing the following props: + - `elements`: the elements defined by the field +- Returns a boolean, indicating if the field is valid or not. Example: @@ -893,7 +939,7 @@ Example: { isValid: ( item, context ) => { return !! item; - } + }; } ``` @@ -918,18 +964,20 @@ Example: Function that indicates if the field should be visible. -- Type: `function`. -- Optional. -- Args - - `item`: the data to be processed -- Returns a `boolean` indicating if the field should be visible (`true`) or not (`false`). +- Type: `function`. +- Optional. +- Args + - `item`: the data to be processed +- Returns a `boolean` indicating if the field should be visible (`true`) or not (`false`). Example: ```js // Custom isVisible function. { - isVisible: ( item ) => { /* Custom implementation. */ } + isVisible: ( item ) => { + /* Custom implementation. */ + }; } ``` @@ -937,54 +985,60 @@ Example: Boolean indicating if the field is sortable. -- Type: `boolean`. -- Optional. -- Defaults to `true`. +- Type: `boolean`. +- Optional. +- Defaults to `true`. Example: ```js -{ enableSorting: true } +{ + enableSorting: true; +} ``` ### `enableHiding` Boolean indicating if the field can be hidden. -- Type: `boolean`. -- Optional. -- Defaults to `true`. +- Type: `boolean`. +- Optional. +- Defaults to `true`. Example: ```js -{ enableHiding: true } +{ + enableHiding: true; +} ``` ### `enableGlobalSearch` Boolean indicating if the field is searchable. -- Type: `boolean`. -- Optional. -- Defaults to `false`. +- Type: `boolean`. +- Optional. +- Defaults to `false`. Example: ```js -{ enableGlobalSearch: true } +{ + enableGlobalSearch: true; +} ``` ### `elements` List of valid values for a field. If provided, it creates a DataViews' filter for the field. DataForm's edit control will also use these values. (See `Edit` field property.) -- Type: `array` of objects. -- Optional. -- Each object can have the following properties: - - `value`: the value to match against the field's value. (Required) - - `label`: the name to display to users. (Required) - - `description`: optional, a longer description of the item. +- Type: `array` of objects. +- Optional. +- Each object can have the following properties: + - `value`: the value to match against the field's value. (Required) + - `label`: the name to display to users. (Required) + - `description`: optional, a longer description of the item. Example: @@ -995,7 +1049,7 @@ Example: { value: '2', label: 'Product B' }, { value: '3', label: 'Product C' }, { value: '4', label: 'Product D' }, - ] + ]; } ``` @@ -1003,11 +1057,11 @@ Example: Configuration of the filters. -- Type: `object`. -- Optional. -- Properties: - - `operators`: the list of operators supported by the field. See "operators" below. A filter will support the `isAny` and `isNone` multi-selection operators by default. - - `isPrimary`: boolean, optional. Indicates if the filter is primary. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter. +- Type: `object`. +- Optional. +- Properties: + - `operators`: the list of operators supported by the field. See "operators" below. A filter will support the `isAny` and `isNone` multi-selection operators by default. + - `isPrimary`: boolean, optional. Indicates if the filter is primary. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter. Operators: @@ -1028,7 +1082,7 @@ Example: // Set a filter as primary. { filterBy: { - isPrimary: true + isPrimary: true; } } ``` @@ -1037,7 +1091,7 @@ Example: // Configure a filter as single-selection. { filterBy: { - operators: [ `is`, `isNot` ] + operators: [ `is`, `isNot` ]; } } ``` @@ -1046,11 +1100,91 @@ Example: // Configure a filter as multi-selection with all the options. { filterBy: { - operators: [ `isAny`, `isNone`, `isAll`, `isNotAll` ] + operators: [ `isAny`, `isNone`, `isAll`, `isNotAll` ]; } } ``` +## Form Field API + +### `id` + +The unique identifier of the field. + +- Type: `string`. +- Required. + +Example: + +```js +{ + id: 'field_id'; +} +``` + +### `layout` + +The same as the `form.type`, either `regular` or `panel` only for the individual field. It defaults to `form.type`. + +- Type: `string`. + +Example: + +```js +{ + id: 'field_id', + layout: 'regular' +} +``` + +### `labelPosition` + +The same as the `form.labelPosition`, either `side`, `top`, or `none` for the individual field. It defaults to `form.labelPosition`. + +- Type: `string`. + +Example: + +```js +{ + id: 'field_id', + labelPosition: 'none' +} +``` + +### `label` + +The label used when displaying a combined field, this requires the use of `children` as well. + +- Type: `string`. + +Example: + +```js +{ + id: 'field_id', + label: 'Combined Field', + children: [ 'field1', 'field2' ] +} +``` + +### `children` + +Groups a set of fields defined within children. For example if you want to display multiple fields within the Panel dropdown you can use children ( see example ). + +- Type: `Array< string | FormField >`. + +Example: + +```js +{ + id: 'status', + layout: 'panel', + label: 'Combined Field', + children: [ 'field1', 'field2' ], +} +``` + ## 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/dataviews/package.json b/packages/dataviews/package.json index c307085bbea078..7f6d96745acab1 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -27,7 +27,8 @@ "exports": { ".": { "types": "./build-types/index.d.ts", - "import": "./build-module/index.js" + "import": "./build-module/index.js", + "default": "./build/index.js" }, "./wp": { "types": "./build-types/index.d.ts", diff --git a/packages/dataviews/src/components/dataviews-context/index.ts b/packages/dataviews/src/components/dataviews-context/index.ts index 4bef3ecdbcbb4a..992048f9097064 100644 --- a/packages/dataviews/src/components/dataviews-context/index.ts +++ b/packages/dataviews/src/components/dataviews-context/index.ts @@ -26,8 +26,10 @@ type DataViewsContextType< Item > = { openedFilter: string | null; setOpenedFilter: ( openedFilter: string | null ) => void; getItemId: ( item: Item ) => string; + getItemLevel?: ( item: Item ) => number; onClickItem?: ( item: Item ) => void; isItemClickable: ( item: Item ) => boolean; + containerWidth: number; }; const DataViewsContext = createContext< DataViewsContextType< any > >( { @@ -45,6 +47,7 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( { openedFilter: null, getItemId: ( item ) => item.id, isItemClickable: () => true, + containerWidth: 0, } ); export default DataViewsContext; diff --git a/packages/dataviews/src/components/dataviews-filters/add-filter.tsx b/packages/dataviews/src/components/dataviews-filters/add-filter.tsx index 94aebb71ea5874..3921fd88eaaa29 100644 --- a/packages/dataviews/src/components/dataviews-filters/add-filter.tsx +++ b/packages/dataviews/src/components/dataviews-filters/add-filter.tsx @@ -33,37 +33,40 @@ export function AddFilterMenu( { view, onChangeView, setOpenedFilter, - trigger, + triggerProps, }: AddFilterProps & { - trigger: React.ReactNode; + triggerProps: React.ComponentProps< typeof Menu.TriggerButton >; } ) { const inactiveFilters = filters.filter( ( filter ) => ! filter.isVisible ); return ( - <Menu trigger={ trigger }> - { inactiveFilters.map( ( filter ) => { - return ( - <Menu.Item - key={ filter.field } - onClick={ () => { - setOpenedFilter( filter.field ); - onChangeView( { - ...view, - page: 1, - filters: [ - ...( view.filters || [] ), - { - field: filter.field, - value: undefined, - operator: filter.operators[ 0 ], - }, - ], - } ); - } } - > - <Menu.ItemLabel>{ filter.name }</Menu.ItemLabel> - </Menu.Item> - ); - } ) } + <Menu> + <Menu.TriggerButton { ...triggerProps } /> + <Menu.Popover> + { inactiveFilters.map( ( filter ) => { + return ( + <Menu.Item + key={ filter.field } + onClick={ () => { + setOpenedFilter( filter.field ); + onChangeView( { + ...view, + page: 1, + filters: [ + ...( view.filters || [] ), + { + field: filter.field, + value: undefined, + operator: filter.operators[ 0 ], + }, + ], + } ); + } } + > + <Menu.ItemLabel>{ filter.name }</Menu.ItemLabel> + </Menu.Item> + ); + } ) } + </Menu.Popover> </Menu> ); } @@ -78,18 +81,19 @@ function AddFilter( const inactiveFilters = filters.filter( ( filter ) => ! filter.isVisible ); return ( <AddFilterMenu - trigger={ - <Button - accessibleWhenDisabled - size="compact" - className="dataviews-filters-button" - variant="tertiary" - disabled={ ! inactiveFilters.length } - ref={ ref } - > - { __( 'Add filter' ) } - </Button> - } + triggerProps={ { + render: ( + <Button + accessibleWhenDisabled + size="compact" + className="dataviews-filters-button" + variant="tertiary" + disabled={ ! inactiveFilters.length } + ref={ ref } + /> + ), + children: __( 'Add filter' ), + } } { ...{ filters, view, onChangeView, setOpenedFilter } } /> ); diff --git a/packages/dataviews/src/components/dataviews-filters/index.tsx b/packages/dataviews/src/components/dataviews-filters/index.tsx index 440df4f17310d6..180e17d4b7f0cc 100644 --- a/packages/dataviews/src/components/dataviews-filters/index.tsx +++ b/packages/dataviews/src/components/dataviews-filters/index.tsx @@ -136,7 +136,7 @@ export function FiltersToggle( { view={ view } onChangeView={ onChangeViewWithFilterVisibility } setOpenedFilter={ setOpenedFilter } - trigger={ buttonComponent } + triggerProps={ { render: buttonComponent } } /> ) : ( <FilterVisibilityToggle diff --git a/packages/dataviews/src/components/dataviews-footer/style.scss b/packages/dataviews/src/components/dataviews-footer/style.scss index cdb1359ccee393..a5cd4dcac9ca02 100644 --- a/packages/dataviews/src/components/dataviews-footer/style.scss +++ b/packages/dataviews/src/components/dataviews-footer/style.scss @@ -11,15 +11,12 @@ z-index: z-index(".dataviews-footer"); } - -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @container (max-width: 430px) { .dataviews-footer { padding: $grid-unit-15 $grid-unit-30; } } -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @container (max-width: 560px) { .dataviews-footer { flex-direction: column !important; diff --git a/packages/dataviews/src/components/dataviews-item-actions/index.tsx b/packages/dataviews/src/components/dataviews-item-actions/index.tsx index abe63e27a15b3b..70df04e4333e6f 100644 --- a/packages/dataviews/src/components/dataviews-item-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-item-actions/index.tsx @@ -75,6 +75,8 @@ function ButtonTrigger< Item >( { <Button label={ label } icon={ action.icon } + disabled={ !! action.disabled } + accessibleWhenDisabled isDestructive={ action.isDestructive } size="compact" onClick={ onClick } @@ -90,7 +92,7 @@ function MenuItemTrigger< Item >( { const label = typeof action.label === 'string' ? action.label : action.label( items ); return ( - <Menu.Item onClick={ onClick }> + <Menu.Item disabled={ action.disabled } onClick={ onClick }> <Menu.ItemLabel>{ label }</Menu.ItemLabel> </Menu.Item> ); @@ -145,13 +147,6 @@ export function ActionsMenuGroup< Item >( { ); } -function hasOnlyOneActionAndIsPrimary< Item >( - primaryActions: Action< Item >[], - actions: Action< Item >[] -) { - return primaryActions.length === 1 && actions.length === 1; -} - export default function ItemActions< Item >( { item, actions, @@ -184,7 +179,8 @@ export default function ItemActions< Item >( { ); } - if ( hasOnlyOneActionAndIsPrimary( primaryActions, actions ) ) { + // If all actions are primary, there is no need to render the dropdown. + if ( primaryActions.length === eligibleActions.length ) { return ( <PrimaryActions item={ item } @@ -229,25 +225,27 @@ function CompactItemActions< Item >( { ); return ( <> - <Menu - trigger={ - <Button - size={ isSmall ? 'small' : 'compact' } - icon={ moreVertical } - label={ __( 'Actions' ) } - accessibleWhenDisabled - disabled={ ! actions.length } - className="dataviews-all-actions-button" - /> - } - placement="bottom-end" - > - <ActionsMenuGroup - actions={ actions } - item={ item } - registry={ registry } - setActiveModalAction={ setActiveModalAction } + <Menu placement="bottom-end"> + <Menu.TriggerButton + render={ + <Button + size={ isSmall ? 'small' : 'compact' } + icon={ moreVertical } + label={ __( 'Actions' ) } + accessibleWhenDisabled + disabled={ ! actions.length } + className="dataviews-all-actions-button" + /> + } /> + <Menu.Popover> + <ActionsMenuGroup + actions={ actions } + item={ item } + registry={ registry } + setActiveModalAction={ setActiveModalAction } + /> + </Menu.Popover> </Menu> { !! activeModalAction && ( <ActionModal diff --git a/packages/dataviews/src/components/dataviews-layout/index.tsx b/packages/dataviews/src/components/dataviews-layout/index.tsx index ebc251eae36a7a..d30b1d39c6524d 100644 --- a/packages/dataviews/src/components/dataviews-layout/index.tsx +++ b/packages/dataviews/src/components/dataviews-layout/index.tsx @@ -21,6 +21,7 @@ export default function DataViewsLayout() { data, fields, getItemId, + getItemLevel, isLoading, view, onChangeView, @@ -40,6 +41,7 @@ export default function DataViewsLayout() { data={ data } fields={ fields } getItemId={ getItemId } + getItemLevel={ getItemLevel } isLoading={ isLoading } onChangeView={ onChangeView } onChangeSelection={ onChangeSelection } diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index 3146064a41922b..0b3512714e14a4 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -69,50 +69,57 @@ function ViewTypeMenu( { } const activeView = VIEW_LAYOUTS.find( ( v ) => view.type === v.type ); return ( - <Menu - trigger={ - <Button - size="compact" - icon={ activeView?.icon } - label={ __( 'Layout' ) } - /> - } - > - { availableLayouts.map( ( layout ) => { - const config = VIEW_LAYOUTS.find( ( v ) => v.type === layout ); - if ( ! config ) { - return null; + <Menu> + <Menu.TriggerButton + render={ + <Button + size="compact" + icon={ activeView?.icon } + label={ __( 'Layout' ) } + /> } - return ( - <Menu.RadioItem - key={ layout } - value={ layout } - name="view-actions-available-view" - checked={ layout === view.type } - hideOnClick - onChange={ ( e: ChangeEvent< HTMLInputElement > ) => { - switch ( e.target.value ) { - case 'list': - case 'grid': - case 'table': - const viewWithoutLayout = { ...view }; - if ( 'layout' in viewWithoutLayout ) { - delete viewWithoutLayout.layout; - } - // @ts-expect-error - return onChangeView( { - ...viewWithoutLayout, - type: e.target.value, - ...defaultLayouts[ e.target.value ], - } ); - } - warning( 'Invalid dataview' ); - } } - > - <Menu.ItemLabel>{ config.label }</Menu.ItemLabel> - </Menu.RadioItem> - ); - } ) } + /> + <Menu.Popover> + { availableLayouts.map( ( layout ) => { + const config = VIEW_LAYOUTS.find( + ( v ) => v.type === layout + ); + if ( ! config ) { + return null; + } + return ( + <Menu.RadioItem + key={ layout } + value={ layout } + name="view-actions-available-view" + checked={ layout === view.type } + hideOnClick + onChange={ ( + e: ChangeEvent< HTMLInputElement > + ) => { + switch ( e.target.value ) { + case 'list': + case 'grid': + case 'table': + const viewWithoutLayout = { ...view }; + if ( 'layout' in viewWithoutLayout ) { + delete viewWithoutLayout.layout; + } + // @ts-expect-error + return onChangeView( { + ...viewWithoutLayout, + type: e.target.value, + ...defaultLayouts[ e.target.value ], + } ); + } + warning( 'Invalid dataview' ); + } } + > + <Menu.ItemLabel>{ config.label }</Menu.ItemLabel> + </Menu.RadioItem> + ); + } ) } + </Menu.Popover> </Menu> ); } @@ -145,6 +152,7 @@ function SortFieldControl() { direction: view?.sort?.direction || 'desc', field: value, }, + showLevels: false, } ); } } /> @@ -187,6 +195,7 @@ function SortDirectionControl() { )?.id || '', }, + showLevels: false, } ); return; } diff --git a/packages/dataviews/src/components/dataviews-view-config/style.scss b/packages/dataviews/src/components/dataviews-view-config/style.scss index 0fd97b916b4aa8..692dddfb7a90b4 100644 --- a/packages/dataviews/src/components/dataviews-view-config/style.scss +++ b/packages/dataviews/src/components/dataviews-view-config/style.scss @@ -43,7 +43,6 @@ display: none; } -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @container (max-width: 500px) { .dataviews-settings-section.dataviews-settings-section { grid-template-columns: repeat(2, 1fr); diff --git a/packages/dataviews/src/components/dataviews/index.tsx b/packages/dataviews/src/components/dataviews/index.tsx index 99d9b6d684b08c..a0a89488136548 100644 --- a/packages/dataviews/src/components/dataviews/index.tsx +++ b/packages/dataviews/src/components/dataviews/index.tsx @@ -8,6 +8,7 @@ import type { ReactNode } from 'react'; */ import { __experimentalHStack as HStack } from '@wordpress/components'; import { useMemo, useState } from '@wordpress/element'; +import { useResizeObserver } from '@wordpress/compose'; /** * Internal dependencies @@ -47,6 +48,7 @@ type DataViewsProps< Item > = { onClickItem?: ( item: Item ) => void; isItemClickable?: ( item: Item ) => boolean; header?: ReactNode; + getItemLevel?: ( item: Item ) => number; } & ( Item extends ItemWithId ? { getItemId?: ( item: Item ) => string } : { getItemId: ( item: Item ) => string } ); @@ -64,6 +66,7 @@ export default function DataViews< Item >( { actions = EMPTY_ARRAY, data, getItemId = defaultGetItemId, + getItemLevel, isLoading = false, paginationInfo, defaultLayouts, @@ -73,6 +76,15 @@ export default function DataViews< Item >( { isItemClickable = defaultIsItemClickable, header, }: DataViewsProps< Item > ) { + const [ containerWidth, setContainerWidth ] = useState( 0 ); + const containerRef = useResizeObserver( + ( resizeObserverEntries: any ) => { + setContainerWidth( + resizeObserverEntries[ 0 ].borderBoxSize[ 0 ].inlineSize + ); + }, + { box: 'border-box' } + ); const [ selectionState, setSelectionState ] = useState< string[] >( [] ); const isUncontrolled = selectionProperty === undefined || onChangeSelection === undefined; @@ -115,11 +127,13 @@ export default function DataViews< Item >( { openedFilter, setOpenedFilter, getItemId, + getItemLevel, isItemClickable, onClickItem, + containerWidth, } } > - <div className="dataviews-wrapper"> + <div className="dataviews-wrapper" ref={ containerRef }> <HStack alignment="top" justify="space-between" diff --git a/packages/dataviews/src/components/dataviews/style.scss b/packages/dataviews/src/components/dataviews/style.scss index b38447094c99a9..3c85115c06dddf 100644 --- a/packages/dataviews/src/components/dataviews/style.scss +++ b/packages/dataviews/src/components/dataviews/style.scss @@ -33,7 +33,6 @@ @include reduce-motion( "transition" ); } -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @container (max-width: 430px) { .dataviews__view-actions, .dataviews-filters__container { diff --git a/packages/dataviews/src/components/form-field-visibility/index.tsx b/packages/dataviews/src/components/form-field-visibility/index.tsx deleted file mode 100644 index 8cea59f11b7aea..00000000000000 --- a/packages/dataviews/src/components/form-field-visibility/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** - * WordPress dependencies - */ -import { useMemo } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import type { NormalizedField } from '../../types'; - -type FormFieldVisibilityProps< Item > = React.PropsWithChildren< { - field: NormalizedField< Item >; - data: Item; -} >; - -export default function FormFieldVisibility< Item >( { - data, - field, - children, -}: FormFieldVisibilityProps< Item > ) { - const isVisible = useMemo( () => { - if ( field.isVisible ) { - return field.isVisible( data ); - } - return true; - }, [ field.isVisible, data ] ); - - if ( ! isVisible ) { - return null; - } - return children; -} diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx index b1f074c5682993..e8f8a46002ebdf 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx @@ -13,6 +13,7 @@ import { Spinner, Flex, FlexItem, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useInstanceId } from '@wordpress/compose'; @@ -20,9 +21,13 @@ import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ +import { unlock } from '../../lock-unlock'; import ItemActions from '../../components/dataviews-item-actions'; import DataViewsSelectionCheckbox from '../../components/dataviews-selection-checkbox'; -import { useHasAPossibleBulkAction } from '../../components/dataviews-bulk-actions'; +import { + useHasAPossibleBulkAction, + useSomeItemHasAPossibleBulkAction, +} from '../../components/dataviews-bulk-actions'; import type { Action, NormalizedField, @@ -32,6 +37,7 @@ import type { import type { SetSelection } from '../../private-types'; import getClickableItemProps from '../utils/get-clickable-item-props'; import { useUpdatedPreviewSizeOnViewportChange } from './preview-size-picker'; +const { Badge } = unlock( componentsPrivateApis ); interface GridItemProps< Item > { view: ViewGridType; @@ -47,6 +53,7 @@ interface GridItemProps< Item > { descriptionField?: NormalizedField< Item >; regularFields: NormalizedField< Item >[]; badgeFields: NormalizedField< Item >[]; + hasBulkActions: boolean; } function GridItem< Item >( { @@ -63,6 +70,7 @@ function GridItem< Item >( { descriptionField, regularFields, badgeFields, + hasBulkActions, }: GridItemProps< Item > ) { const { showTitle = true, showMedia = true, showDescription = true } = view; const hasBulkAction = useHasAPossibleBulkAction( actions, item ); @@ -135,7 +143,7 @@ function GridItem< Item >( { { renderedMediaField } </div> ) } - { showMedia && renderedMediaField && ( + { hasBulkActions && showMedia && renderedMediaField && ( <DataViewsSelectionCheckbox item={ item } selection={ selection } @@ -152,7 +160,9 @@ function GridItem< Item >( { <div { ...clickableTitleItemProps } { ...titleA11yProps }> { renderedTitleField } </div> - <ItemActions item={ item } actions={ actions } isCompact /> + { !! actions?.length && ( + <ItemActions item={ item } actions={ actions } isCompact /> + ) } </HStack> <VStack spacing={ 1 }> { showDescription && descriptionField?.render && ( @@ -168,12 +178,12 @@ function GridItem< Item >( { > { badgeFields.map( ( field ) => { return ( - <FlexItem + <Badge key={ field.id } className="dataviews-view-grid__field-value" > <field.render item={ item } /> - </FlexItem> + </Badge> ); } ) } </HStack> @@ -258,6 +268,7 @@ export default function ViewGrid< Item >( { ); const hasData = !! data?.length; const updatedPreviewSize = useUpdatedPreviewSizeOnViewportChange(); + const hasBulkActions = useSomeItemHasAPossibleBulkAction( actions, data ); const usedPreviewSize = updatedPreviewSize || view.layout?.previewSize; const gridStyle = usedPreviewSize ? { @@ -292,6 +303,7 @@ export default function ViewGrid< Item >( { descriptionField={ descriptionField } regularFields={ regularFields } badgeFields={ badgeFields } + hasBulkActions={ hasBulkActions } /> ); } ) } diff --git a/packages/dataviews/src/dataviews-layouts/grid/preview-size-picker.tsx b/packages/dataviews/src/dataviews-layouts/grid/preview-size-picker.tsx index b48c6422bd6b37..027632090b31b4 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/preview-size-picker.tsx +++ b/packages/dataviews/src/dataviews-layouts/grid/preview-size-picker.tsx @@ -3,7 +3,6 @@ */ import { RangeControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useViewportMatch } from '@wordpress/compose'; import { useMemo, useContext } from '@wordpress/element'; /** @@ -12,7 +11,9 @@ import { useMemo, useContext } from '@wordpress/element'; import DataViewsContext from '../../components/dataviews-context'; import type { ViewGrid } from '../../types'; -const viewportBreaks = { +const viewportBreaks: { + [ key: string ]: { min: number; max: number; default: number }; +} = { xhuge: { min: 3, max: 6, default: 5 }, huge: { min: 2, max: 4, default: 4 }, xlarge: { min: 2, max: 3, default: 3 }, @@ -20,38 +21,35 @@ const viewportBreaks = { mobile: { min: 1, max: 2, default: 2 }, }; -function useViewPortBreakpoint() { - const isXHuge = useViewportMatch( 'xhuge', '>=' ); - const isHuge = useViewportMatch( 'huge', '>=' ); - const isXlarge = useViewportMatch( 'xlarge', '>=' ); - const isLarge = useViewportMatch( 'large', '>=' ); - const isMobile = useViewportMatch( 'mobile', '>=' ); +/** + * Breakpoints were adjusted from media queries breakpoints to account for + * the sidebar width. This was done to match the existing styles we had. + */ +const BREAKPOINTS = { + xhuge: 1520, + huge: 1140, + xlarge: 780, + large: 480, + mobile: 0, +}; - if ( isXHuge ) { - return 'xhuge'; - } - if ( isHuge ) { - return 'huge'; - } - if ( isXlarge ) { - return 'xlarge'; - } - if ( isLarge ) { - return 'large'; - } - if ( isMobile ) { - return 'mobile'; +function useViewPortBreakpoint() { + const containerWidth = useContext( DataViewsContext ).containerWidth; + for ( const [ key, value ] of Object.entries( BREAKPOINTS ) ) { + if ( containerWidth >= value ) { + return key; + } } - return null; + return 'mobile'; } export function useUpdatedPreviewSizeOnViewportChange() { - const viewport = useViewPortBreakpoint(); const view = useContext( DataViewsContext ).view as ViewGrid; + const viewport = useViewPortBreakpoint(); return useMemo( () => { const previewSize = view.layout?.previewSize; let newPreviewSize; - if ( ! viewport || ! previewSize ) { + if ( ! previewSize ) { return; } const breakValues = viewportBreaks[ viewport ]; @@ -69,9 +67,8 @@ export default function PreviewSizePicker() { const viewport = useViewPortBreakpoint(); const context = useContext( DataViewsContext ); const view = context.view as ViewGrid; - const breakValues = viewportBreaks[ viewport || 'mobile' ]; + const breakValues = viewportBreaks[ viewport ]; const previewSizeToUse = view.layout?.previewSize || breakValues.default; - const marks = useMemo( () => Array.from( @@ -84,11 +81,9 @@ export default function PreviewSizePicker() { ), [ breakValues ] ); - - if ( ! viewport ) { + if ( viewport === 'mobile' ) { return null; } - return ( <RangeControl __nextHasNoMarginBottom diff --git a/packages/dataviews/src/dataviews-layouts/grid/style.scss b/packages/dataviews/src/dataviews-layouts/grid/style.scss index e9fcb472dc3186..333e6e9a4caf9f 100644 --- a/packages/dataviews/src/dataviews-layouts/grid/style.scss +++ b/packages/dataviews/src/dataviews-layouts/grid/style.scss @@ -3,6 +3,7 @@ grid-template-rows: max-content; padding: 0 $grid-unit-60 $grid-unit-30; transition: padding ease-out 0.1s; + container-type: inline-size; @include reduce-motion("transition"); @@ -30,11 +31,16 @@ .dataviews-view-grid__fields .dataviews-view-grid__field .dataviews-view-grid__field-value { color: $gray-900; } - - .dataviews-view-grid__media::after { - background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08); - box-shadow: inset 0 0 0 $border-width var(--wp-admin-theme-color); - } + } + &.is-selected .dataviews-view-grid__media::after, + .dataviews-view-grid__media:focus::after { + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08); + } + &.is-selected .dataviews-view-grid__media::after { + box-shadow: inset 0 0 0 $border-width var(--wp-admin-theme-color); + } + .dataviews-view-grid__media:focus::after { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); } } @@ -106,36 +112,29 @@ &:not(:empty) { padding-bottom: $grid-unit-15; } - - .dataviews-view-grid__field-value { - width: fit-content; - background: $gray-100; - padding: 0 $grid-unit-10; - min-height: $grid-unit-30; - border-radius: $radius-small; - display: flex; - align-items: center; - font-size: 12px; - } } } .dataviews-view-grid.dataviews-view-grid { - grid-template-columns: repeat(1, minmax(0, 1fr)); - - @include break-mobile() { + /** + * Breakpoints were adjusted from media queries breakpoints to account for + * the sidebar width. This was done to match the existing styles we had. + */ + @container (max-width: 480px) { + grid-template-columns: repeat(1, minmax(0, 1fr)); + padding-left: $grid-unit-30; + padding-right: $grid-unit-30; + } + @container (min-width: 480px) { grid-template-columns: repeat(2, minmax(0, 1fr)); } - - @include break-xlarge() { + @container (min-width: 780px) { grid-template-columns: repeat(3, minmax(0, 1fr)); } - - @include break-huge() { + @container (min-width: 1140px) { grid-template-columns: repeat(4, minmax(0, 1fr)); } - - @include break-xhuge() { + @container (min-width: 1520px) { grid-template-columns: repeat(5, minmax(0, 1fr)); } } @@ -158,14 +157,6 @@ top: $grid-unit-10; } -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ -@container (max-width: 430px) { - .dataviews-view-grid { - padding-left: $grid-unit-30; - padding-right: $grid-unit-30; - } -} - .dataviews-view-grid__media--clickable { cursor: pointer; } diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx index fd6cdff6dbcdc6..dadc53b5d733a7 100644 --- a/packages/dataviews/src/dataviews-layouts/list/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx @@ -101,6 +101,8 @@ function PrimaryActionGridCell< Item >( { render={ <Button label={ label } + disabled={ !! primaryAction.disabled } + accessibleWhenDisabled icon={ primaryAction.icon } isDestructive={ primaryAction.isDestructive } size="small" @@ -124,6 +126,8 @@ function PrimaryActionGridCell< Item >( { render={ <Button label={ label } + disabled={ !! primaryAction.disabled } + accessibleWhenDisabled icon={ primaryAction.icon } isDestructive={ primaryAction.isDestructive } size="small" @@ -215,32 +219,36 @@ function ListItem< Item >( { ) } { ! hasOnlyOnePrimaryAction && ( <div role="gridcell"> - <Menu - trigger={ - <Composite.Item - id={ generateDropdownTriggerCompositeId( - idPrefix - ) } - render={ - <Button - size="small" - icon={ moreVertical } - label={ __( 'Actions' ) } - accessibleWhenDisabled - disabled={ ! actions.length } - onKeyDown={ onDropdownTriggerKeyDown } - /> - } - /> - } - placement="bottom-end" - > - <ActionsMenuGroup - actions={ eligibleActions } - item={ item } - registry={ registry } - setActiveModalAction={ setActiveModalAction } + <Menu placement="bottom-end"> + <Menu.TriggerButton + render={ + <Composite.Item + id={ generateDropdownTriggerCompositeId( + idPrefix + ) } + render={ + <Button + size="small" + icon={ moreVertical } + label={ __( 'Actions' ) } + accessibleWhenDisabled + disabled={ ! actions.length } + onKeyDown={ + onDropdownTriggerKeyDown + } + /> + } + /> + } /> + <Menu.Popover> + <ActionsMenuGroup + actions={ eligibleActions } + item={ item } + registry={ registry } + setActiveModalAction={ setActiveModalAction } + /> + </Menu.Popover> </Menu> { !! activeModalAction && ( <ActionModal @@ -257,7 +265,7 @@ function ListItem< Item >( { return ( <Composite.Row ref={ itemRef } - render={ <li /> } + render={ <div /> } role="row" className={ clsx( { 'is-selected': isSelected, @@ -482,7 +490,7 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { return ( <Composite id={ baseId } - render={ <ul /> } + render={ <div /> } className="dataviews-view-list" role="grid" activeId={ activeCompositeId } diff --git a/packages/dataviews/src/dataviews-layouts/list/style.scss b/packages/dataviews/src/dataviews-layouts/list/style.scss index 82ef269d46964e..e892006faecb00 100644 --- a/packages/dataviews/src/dataviews-layouts/list/style.scss +++ b/packages/dataviews/src/dataviews-layouts/list/style.scss @@ -1,11 +1,11 @@ -ul.dataviews-view-list { +div.dataviews-view-list { list-style-type: none; } .dataviews-view-list { margin: 0 0 auto; - li { + div[role="row"] { margin: 0; border-top: 1px solid $gray-100; @@ -45,7 +45,7 @@ ul.dataviews-view-list { &.is-selected.is-selected { border-top: 1px solid rgba(var(--wp-admin-theme-color--rgb), 0.12); - & + li { + & + div[role="row"] { border-top: 1px solid rgba(var(--wp-admin-theme-color--rgb), 0.12); } } @@ -69,8 +69,8 @@ ul.dataviews-view-list { } - li.is-selected, - li.is-selected:focus-within { + div[role="row"].is-selected, + div[role="row"].is-selected:focus-within { .dataviews-view-list__item-wrapper { background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04); color: $gray-900; diff --git a/packages/dataviews/src/dataviews-layouts/table/column-header-menu.tsx b/packages/dataviews/src/dataviews-layouts/table/column-header-menu.tsx index 763cf83b5c2f93..1d8d22193bbd07 100644 --- a/packages/dataviews/src/dataviews-layouts/table/column-header-menu.tsx +++ b/packages/dataviews/src/dataviews-layouts/table/column-header-menu.tsx @@ -93,168 +93,172 @@ const _HeaderMenu = forwardRef( function HeaderMenu< Item >( ! field.filterBy?.isPrimary; return ( - <Menu - align="start" - trigger={ - <Button - size="compact" - className="dataviews-view-table-header-button" - ref={ ref } - variant="tertiary" - > - { header } - { view.sort && isSorted && ( - <span aria-hidden="true"> - { sortArrows[ view.sort.direction ] } - </span> - ) } - </Button> - } - style={ { minWidth: '240px' } } - > - <WithMenuSeparators> - { isSortable && ( - <Menu.Group> - { SORTING_DIRECTIONS.map( - ( direction: SortDirection ) => { - const isChecked = - view.sort && - isSorted && - view.sort.direction === direction; + <Menu> + <Menu.TriggerButton + render={ + <Button + size="compact" + className="dataviews-view-table-header-button" + ref={ ref } + variant="tertiary" + /> + } + > + { header } + { view.sort && isSorted && ( + <span aria-hidden="true"> + { sortArrows[ view.sort.direction ] } + </span> + ) } + </Menu.TriggerButton> + <Menu.Popover style={ { minWidth: '240px' } }> + <WithMenuSeparators> + { isSortable && ( + <Menu.Group> + { SORTING_DIRECTIONS.map( + ( direction: SortDirection ) => { + const isChecked = + view.sort && + isSorted && + view.sort.direction === direction; - const value = `${ fieldId }-${ direction }`; + const value = `${ fieldId }-${ direction }`; - return ( - <Menu.RadioItem - key={ value } - // All sorting radio items share the same name, so that - // selecting a sorting option automatically deselects the - // previously selected one, even if it is displayed in - // another submenu. The field and direction are passed via - // the `value` prop. - name="view-table-sorting" - value={ value } - checked={ isChecked } - onChange={ () => { - onChangeView( { - ...view, - sort: { - field: fieldId, - direction, - }, - } ); - } } - > - <Menu.ItemLabel> - { sortLabels[ direction ] } - </Menu.ItemLabel> - </Menu.RadioItem> - ); - } - ) } - </Menu.Group> - ) } - { canAddFilter && ( - <Menu.Group> - <Menu.Item - prefix={ <Icon icon={ funnel } /> } - onClick={ () => { - setOpenedFilter( fieldId ); - onChangeView( { - ...view, - page: 1, - filters: [ - ...( view.filters || [] ), - { - field: fieldId, - value: undefined, - operator: operators[ 0 ], - }, - ], - } ); - } } - > - <Menu.ItemLabel> - { __( 'Add filter' ) } - </Menu.ItemLabel> - </Menu.Item> - </Menu.Group> - ) } - { ( canMove || isHidable ) && field && ( - <Menu.Group> - { canMove && ( + return ( + <Menu.RadioItem + key={ value } + // All sorting radio items share the same name, so that + // selecting a sorting option automatically deselects the + // previously selected one, even if it is displayed in + // another submenu. The field and direction are passed via + // the `value` prop. + name="view-table-sorting" + value={ value } + checked={ isChecked } + onChange={ () => { + onChangeView( { + ...view, + sort: { + field: fieldId, + direction, + }, + showLevels: false, + } ); + } } + > + <Menu.ItemLabel> + { sortLabels[ direction ] } + </Menu.ItemLabel> + </Menu.RadioItem> + ); + } + ) } + </Menu.Group> + ) } + { canAddFilter && ( + <Menu.Group> <Menu.Item - prefix={ <Icon icon={ arrowLeft } /> } - disabled={ index < 1 } + prefix={ <Icon icon={ funnel } /> } onClick={ () => { + setOpenedFilter( fieldId ); onChangeView( { ...view, - fields: [ - ...( visibleFieldIds.slice( - 0, - index - 1 - ) ?? [] ), - fieldId, - visibleFieldIds[ index - 1 ], - ...visibleFieldIds.slice( - index + 1 - ), + page: 1, + filters: [ + ...( view.filters || [] ), + { + field: fieldId, + value: undefined, + operator: operators[ 0 ], + }, ], } ); } } > <Menu.ItemLabel> - { __( 'Move left' ) } + { __( 'Add filter' ) } </Menu.ItemLabel> </Menu.Item> - ) } - { canMove && ( - <Menu.Item - prefix={ <Icon icon={ arrowRight } /> } - disabled={ index >= visibleFieldIds.length - 1 } - onClick={ () => { - onChangeView( { - ...view, - fields: [ - ...( visibleFieldIds.slice( - 0, - index - ) ?? [] ), - visibleFieldIds[ index + 1 ], - fieldId, - ...visibleFieldIds.slice( - index + 2 + </Menu.Group> + ) } + { ( canMove || isHidable ) && field && ( + <Menu.Group> + { canMove && ( + <Menu.Item + prefix={ <Icon icon={ arrowLeft } /> } + disabled={ index < 1 } + onClick={ () => { + onChangeView( { + ...view, + fields: [ + ...( visibleFieldIds.slice( + 0, + index - 1 + ) ?? [] ), + fieldId, + visibleFieldIds[ index - 1 ], + ...visibleFieldIds.slice( + index + 1 + ), + ], + } ); + } } + > + <Menu.ItemLabel> + { __( 'Move left' ) } + </Menu.ItemLabel> + </Menu.Item> + ) } + { canMove && ( + <Menu.Item + prefix={ <Icon icon={ arrowRight } /> } + disabled={ + index >= visibleFieldIds.length - 1 + } + onClick={ () => { + onChangeView( { + ...view, + fields: [ + ...( visibleFieldIds.slice( + 0, + index + ) ?? [] ), + visibleFieldIds[ index + 1 ], + fieldId, + ...visibleFieldIds.slice( + index + 2 + ), + ], + } ); + } } + > + <Menu.ItemLabel> + { __( 'Move right' ) } + </Menu.ItemLabel> + </Menu.Item> + ) } + { isHidable && field && ( + <Menu.Item + prefix={ <Icon icon={ unseen } /> } + onClick={ () => { + onHide( field ); + onChangeView( { + ...view, + fields: visibleFieldIds.filter( + ( id ) => id !== fieldId ), - ], - } ); - } } - > - <Menu.ItemLabel> - { __( 'Move right' ) } - </Menu.ItemLabel> - </Menu.Item> - ) } - { isHidable && field && ( - <Menu.Item - prefix={ <Icon icon={ unseen } /> } - onClick={ () => { - onHide( field ); - onChangeView( { - ...view, - fields: visibleFieldIds.filter( - ( id ) => id !== fieldId - ), - } ); - } } - > - <Menu.ItemLabel> - { __( 'Hide column' ) } - </Menu.ItemLabel> - </Menu.Item> - ) } - </Menu.Group> - ) } - </WithMenuSeparators> + } ); + } } + > + <Menu.ItemLabel> + { __( 'Hide column' ) } + </Menu.ItemLabel> + </Menu.Item> + ) } + </Menu.Group> + ) } + </WithMenuSeparators> + </Menu.Popover> </Menu> ); } ); diff --git a/packages/dataviews/src/dataviews-layouts/table/column-primary.tsx b/packages/dataviews/src/dataviews-layouts/table/column-primary.tsx index 6db65be72bdd4c..6ac4057b0973ba 100644 --- a/packages/dataviews/src/dataviews-layouts/table/column-primary.tsx +++ b/packages/dataviews/src/dataviews-layouts/table/column-primary.tsx @@ -14,6 +14,7 @@ import getClickableItemProps from '../utils/get-clickable-item-props'; function ColumnPrimary< Item >( { item, + level, titleField, mediaField, descriptionField, @@ -21,6 +22,7 @@ function ColumnPrimary< Item >( { isItemClickable, }: { item: Item; + level?: number; titleField?: NormalizedField< Item >; mediaField?: NormalizedField< Item >; descriptionField?: NormalizedField< Item >; @@ -44,6 +46,11 @@ function ColumnPrimary< Item >( { <VStack spacing={ 0 }> { titleField && ( <div { ...clickableProps }> + { level !== undefined && ( + <span className="dataviews-view-table__level"> + { '—'.repeat( level ) }&nbsp; + </span> + ) } <titleField.render item={ item } /> </div> ) } diff --git a/packages/dataviews/src/dataviews-layouts/table/index.tsx b/packages/dataviews/src/dataviews-layouts/table/index.tsx index b010b3ff154fbb..855e0584563b71 100644 --- a/packages/dataviews/src/dataviews-layouts/table/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/table/index.tsx @@ -40,6 +40,7 @@ interface TableColumnFieldProps< Item > { interface TableRowProps< Item > { hasBulkActions: boolean; item: Item; + level?: number; actions: Action< Item >[]; fields: NormalizedField< Item >[]; id: string; @@ -75,6 +76,7 @@ function TableColumnField< Item >( { function TableRow< Item >( { hasBulkActions, item, + level, actions, fields, id, @@ -160,6 +162,7 @@ function TableRow< Item >( { <td> <ColumnPrimary item={ item } + level={ level } titleField={ showTitle ? titleField : undefined } mediaField={ showMedia ? mediaField : undefined } descriptionField={ @@ -210,6 +213,7 @@ function ViewTable< Item >( { data, fields, getItemId, + getItemLevel, isLoading = false, onChangeView, onChangeSelection, @@ -375,6 +379,12 @@ function ViewTable< Item >( { <TableRow key={ getItemId( item ) } item={ item } + level={ + view.showLevels && + typeof getItemLevel === 'function' + ? getItemLevel( item ) + : undefined + } hasBulkActions={ hasBulkActions } actions={ actions } fields={ fields } diff --git a/packages/dataviews/src/dataviews-layouts/table/style.scss b/packages/dataviews/src/dataviews-layouts/table/style.scss index 5a713dd428c127..5a4ac01b566f74 100644 --- a/packages/dataviews/src/dataviews-layouts/table/style.scss +++ b/packages/dataviews/src/dataviews-layouts/table/style.scss @@ -203,7 +203,6 @@ } } -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @container (max-width: 430px) { .dataviews-view-table tr td:first-child, .dataviews-view-table tr th:first-child { diff --git a/packages/dataviews/src/test/dataform.tsx b/packages/dataviews/src/test/dataform.tsx new file mode 100644 index 00000000000000..534151a0a4ab58 --- /dev/null +++ b/packages/dataviews/src/test/dataform.tsx @@ -0,0 +1,348 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import Dataform from '../components/dataform/index'; + +const noop = () => {}; + +const fields = [ + { + id: 'title', + label: 'Title', + type: 'text' as const, + }, + { + id: 'order', + label: 'Order', + type: 'integer' as const, + }, + { + id: 'author', + label: 'Author', + type: 'integer' as const, + elements: [ + { value: 1, label: 'Jane' }, + { value: 2, label: 'John' }, + ], + }, +]; + +const form = { + fields: [ 'title', 'order', 'author' ], +}; + +const data = { + title: 'Hello World', + author: 1, + order: 1, +}; + +const fieldsSelector = { + title: { + view: () => + screen.getByRole( 'button', { + name: /edit title/i, + } ), + edit: () => + screen.getByRole( 'textbox', { + name: /title/i, + } ), + }, + author: { + view: () => + screen.getByRole( 'button', { + name: /edit author/i, + } ), + edit: () => + screen.queryByRole( 'combobox', { + name: /author/i, + } ), + }, + order: { + view: () => + screen.getByRole( 'button', { + name: /edit order/i, + } ), + edit: () => + screen.getByRole( 'spinbutton', { + name: /order/i, + } ), + }, +}; + +describe( 'DataForm component', () => { + describe( 'in regular mode', () => { + it( 'should display fields', () => { + render( + <Dataform + onChange={ noop } + fields={ fields } + form={ form } + data={ data } + /> + ); + + expect( fieldsSelector.title.edit() ).toBeInTheDocument(); + expect( fieldsSelector.order.edit() ).toBeInTheDocument(); + expect( fieldsSelector.author.edit() ).toBeInTheDocument(); + } ); + + it( 'should render custom Edit component', () => { + const fieldsWithCustomEditComponent = fields.map( ( field ) => { + if ( field.id === 'title' ) { + return { + ...field, + Edit: () => { + return <span>This is the Title Field</span>; + }, + }; + } + return field; + } ); + + render( + <Dataform + onChange={ noop } + fields={ fieldsWithCustomEditComponent } + form={ form } + data={ data } + /> + ); + + const titleField = screen.getByText( 'This is the Title Field' ); + expect( titleField ).toBeInTheDocument(); + } ); + + it( 'should call onChange with the correct value for each typed character', async () => { + const onChange = jest.fn(); + render( + <Dataform + onChange={ onChange } + fields={ fields } + form={ form } + data={ { ...data, title: '' } } + /> + ); + + const titleInput = fieldsSelector.title.edit(); + const user = userEvent.setup(); + await user.clear( titleInput ); + expect( titleInput ).toHaveValue( '' ); + const newValue = 'Hello folks!'; + await user.type( titleInput, newValue ); + expect( onChange ).toHaveBeenCalledTimes( newValue.length ); + for ( let i = 0; i < newValue.length; i++ ) { + expect( onChange ).toHaveBeenNthCalledWith( i + 1, { + title: newValue[ i ], + } ); + } + } ); + + it( 'should wrap fields in HStack when labelPosition is set to side', async () => { + const { container } = render( + <Dataform + onChange={ noop } + fields={ fields } + form={ { ...form, labelPosition: 'side' } } + data={ data } + /> + ); + + expect( + // It is used here to ensure that the fields are wrapped in HStack. This happens when the labelPosition is set to side. + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + container.querySelectorAll( "[data-wp-component='HStack']" ) + ).toHaveLength( 3 ); + } ); + + it( 'should render combined fields correctly', async () => { + const formWithCombinedFields = { + fields: [ + 'order', + { + id: 'title', + children: [ 'title', 'author' ], + label: "Title and author's name", + }, + ], + }; + + render( + <Dataform + onChange={ noop } + fields={ fields } + form={ formWithCombinedFields } + data={ data } + /> + ); + + expect( + screen.getByText( "Title and author's name" ) + ).toBeInTheDocument(); + } ); + } ); + + describe( 'in panel mode', () => { + const formPanelMode = { + ...form, + type: 'panel' as const, + }; + it( 'should display fields', async () => { + render( + <Dataform + onChange={ noop } + fields={ fields } + form={ formPanelMode } + data={ data } + /> + ); + + const user = await userEvent.setup(); + + for ( const field of Object.values( fieldsSelector ) ) { + const button = field.view(); + await user.click( button ); + expect( field.edit() ).toBeInTheDocument(); + } + } ); + + it( 'should call onChange with the correct value for each typed character', async () => { + const onChange = jest.fn(); + render( + <Dataform + onChange={ onChange } + fields={ fields } + form={ formPanelMode } + data={ { ...data, title: '' } } + /> + ); + + const titleButton = fieldsSelector.title.view(); + const user = await userEvent.setup(); + await user.click( titleButton ); + const input = fieldsSelector.title.edit(); + expect( input ).toHaveValue( '' ); + const newValue = 'Hello folks!'; + await user.type( input, newValue ); + expect( onChange ).toHaveBeenCalledTimes( newValue.length ); + for ( let i = 0; i < newValue.length; i++ ) { + expect( onChange ).toHaveBeenNthCalledWith( i + 1, { + title: newValue[ i ], + } ); + } + } ); + + it( 'should wrap fields in HStack when labelPosition is set to side', async () => { + const { container } = render( + <Dataform + onChange={ noop } + fields={ fields } + form={ { ...formPanelMode, labelPosition: 'side' } } + data={ data } + /> + ); + + expect( + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access + container.querySelectorAll( "[data-wp-component='HStack']" ) + ).toHaveLength( 3 ); + } ); + + it( 'should render combined fields correctly', async () => { + const formWithCombinedFields = { + ...formPanelMode, + fields: [ + 'order', + { + id: 'title', + children: [ 'title', 'author' ], + label: "Title and author's name", + }, + ], + }; + + render( + <Dataform + onChange={ noop } + fields={ fields } + form={ formWithCombinedFields } + data={ data } + /> + ); + + const button = screen.getByRole( 'button', { + name: /edit title and author's name/i, + } ); + const user = await userEvent.setup(); + await user.click( button ); + expect( fieldsSelector.title.edit() ).toBeInTheDocument(); + expect( fieldsSelector.author.edit() ).toBeInTheDocument(); + } ); + + it( 'should render custom render component', async () => { + const fieldsWithCustomRenderFunction = fields.map( ( field ) => { + return { + ...field, + render: () => { + return <span>This is the { field.id } field</span>; + }, + }; + } ); + + render( + <Dataform + onChange={ noop } + fields={ fieldsWithCustomRenderFunction } + form={ formPanelMode } + data={ data } + /> + ); + + const titleField = screen.getByText( 'This is the title field' ); + const orderField = screen.getByText( 'This is the order field' ); + const authorField = screen.getByText( 'This is the author field' ); + expect( titleField ).toBeInTheDocument(); + expect( orderField ).toBeInTheDocument(); + expect( authorField ).toBeInTheDocument(); + } ); + + it( 'should render custom Edit component', async () => { + const fieldsWithTitleCustomEditComponent = fields.map( + ( field ) => { + if ( field.id === 'title' ) { + return { + ...field, + Edit: () => { + return <span>This is the Title Field</span>; + }, + }; + } + return field; + } + ); + + render( + <Dataform + onChange={ noop } + fields={ fieldsWithTitleCustomEditComponent } + form={ formPanelMode } + data={ data } + /> + ); + + const titleField = screen.getByText( data.title ); + const user = await userEvent.setup(); + await user.click( titleField ); + const titleEditField = screen.getByText( + 'This is the Title Field' + ); + expect( titleEditField ).toBeInTheDocument(); + } ); + } ); +} ); diff --git a/packages/dataviews/src/test/dataviews.tsx b/packages/dataviews/src/test/dataviews.tsx new file mode 100644 index 00000000000000..fb55bf8064622f --- /dev/null +++ b/packages/dataviews/src/test/dataviews.tsx @@ -0,0 +1,380 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * WordPress dependencies + */ +import { useMemo, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import DataViews from '../components/dataviews'; +import { LAYOUT_GRID, LAYOUT_LIST, LAYOUT_TABLE } from '../constants'; +import type { Action, View } from '../types'; +import { filterSortAndPaginate } from '../filter-and-sort-data-view'; + +type Data = { + id: number; + title: string; + author?: number; + order?: number; +}; + +const DEFAULT_VIEW = { + type: 'table' as const, + search: '', + page: 1, + perPage: 10, + layout: {}, + filters: [], +}; + +const defaultLayouts = { + [ LAYOUT_TABLE ]: {}, + [ LAYOUT_GRID ]: {}, + [ LAYOUT_LIST ]: {}, +}; + +const fields = [ + { + id: 'title', + label: 'Title', + type: 'text' as const, + }, + { + id: 'order', + label: 'Order', + type: 'integer' as const, + }, + { + id: 'author', + label: 'Author', + type: 'integer' as const, + elements: [ + { value: 1, label: 'Jane' }, + { value: 2, label: 'John' }, + ], + }, + { + label: 'Image', + id: 'image', + render: ( { item }: { item: Data } ) => { + return ( + <svg + width="400" + height="180" + data-testid={ 'image-field-' + item.id } + > + <rect + x="50" + y="20" + rx="20" + ry="20" + width="150" + height="150" + style={ { fill: 'red', opacity: 0.5 } } + /> + </svg> + ); + }, + enableSorting: false, + }, +]; + +const actions: Action< Data >[] = [ + { + id: 'delete', + label: 'Delete', + isDestructive: true, + supportsBulk: true, + RenderModal: () => <div>Modal Content</div>, + }, +]; + +const data: Data[] = [ + { + id: 1, + title: 'Hello World', + author: 1, + order: 1, + }, + { + id: 2, + title: 'Homepage', + author: 2, + order: 1, + }, + { + id: 3, + title: 'Posts', + author: 2, + order: 1, + }, +]; + +function DataViewWrapper( { + view: additionalView, + ...props +}: Partial< Parameters< typeof DataViews< Data > >[ 0 ] > ) { + const [ view, setView ] = useState< View >( { + ...DEFAULT_VIEW, + fields: [ 'title', 'order', 'author' ], + ...additionalView, + } ); + + const { data: shownData, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( data, view, props.fields || fields ); + }, [ view, props.fields ] ); + + const dataViewProps = { + getItemId: ( item: Data ) => item.id.toString(), + paginationInfo, + data: shownData, + view, + fields, + onChangeView: setView, + actions: [], + defaultLayouts, + ...props, + }; + + return <DataViews { ...dataViewProps } />; +} + +// jest.useFakeTimers(); + +describe( 'DataViews component', () => { + it( 'should show "No results" if data is empty', () => { + render( <DataViewWrapper data={ [] } /> ); + expect( screen.getByText( 'No results' ) ).toBeInTheDocument(); + } ); + + it( 'should filter results by "search" text, if field has enableGlobalSearch set to true', async () => { + const fieldsWithSearch = [ + { + ...fields[ 0 ], + enableGlobalSearch: true, + }, + fields[ 1 ], + ]; + render( + <DataViewWrapper + fields={ fieldsWithSearch } + view={ { ...DEFAULT_VIEW, search: 'Hello' } } + /> + ); + // Row count includes header. + expect( screen.getAllByRole( 'row' ).length ).toEqual( 2 ); + expect( screen.getByText( 'Hello World' ) ).toBeInTheDocument(); + } ); + + it( 'should display matched element label if field contains elements list', () => { + render( + <DataViewWrapper + data={ [ { id: 1, author: 3, title: 'Hello World' } ] } + fields={ [ + { + id: 'author', + label: 'Author', + type: 'integer' as const, + elements: [ + { value: 1, label: 'Jane' }, + { value: 2, label: 'John' }, + { value: 3, label: 'Tim' }, + ], + }, + ] } + /> + ); + expect( screen.getByText( 'Tim' ) ).toBeInTheDocument(); + } ); + + it( 'should render custom render function if defined in field definition', () => { + render( + <DataViewWrapper + data={ [ { id: 1, title: 'Test Title' } ] } + fields={ [ + { + id: 'title', + label: 'Title', + type: 'text' as const, + render: ( { item }: { item: Data } ) => { + return item.title?.toUpperCase(); + }, + }, + ] } + /> + ); + expect( screen.getByText( 'TEST TITLE' ) ).toBeInTheDocument(); + } ); + + describe( 'in table view', () => { + it( 'should display columns for each field', () => { + render( <DataViewWrapper /> ); + const displayedColumnFields = fields.filter( ( field ) => + [ 'title', 'order', 'author' ].includes( field.id ) + ); + for ( const field of displayedColumnFields ) { + expect( + screen.getByRole( 'button', { name: field.label } ) + ).toBeInTheDocument(); + } + } ); + + it( 'should display the passed in data', () => { + render( <DataViewWrapper /> ); + for ( const item of data ) { + expect( + screen.getAllByText( item.title )[ 0 ] + ).toBeInTheDocument(); + } + } ); + + it( 'should display title column if defined using titleField', () => { + render( + <DataViewWrapper + view={ { + ...DEFAULT_VIEW, + fields: [ 'order', 'author' ], + titleField: 'title', + } } + /> + ); + for ( const item of data ) { + expect( + screen.getAllByText( item.title )[ 0 ] + ).toBeInTheDocument(); + } + } ); + + it( 'should render actions column if actions are supported and passed in', () => { + render( <DataViewWrapper actions={ actions } /> ); + expect( screen.getByText( 'Actions' ) ).toBeInTheDocument(); + } ); + + it( 'should trigger the onClickItem callback if isItemClickable returns true and title field is clicked', async () => { + const onClickItemCallback = jest.fn(); + + render( + <DataViewWrapper + view={ { + ...DEFAULT_VIEW, + fields: [ 'author' ], + titleField: 'title', + } } + actions={ actions } + isItemClickable={ () => true } + onClickItem={ onClickItemCallback } + /> + ); + const titleField = screen.getByText( data[ 0 ].title ); + const user = userEvent.setup(); + await user.click( titleField ); + expect( onClickItemCallback ).toHaveBeenCalledWith( data[ 0 ] ); + } ); + } ); + + describe( 'in grid view', () => { + it( 'should display the passed in data', () => { + render( + <DataViewWrapper + view={ { + type: 'grid', + } } + /> + ); + for ( const item of data ) { + expect( + screen.getAllByText( item.title )[ 0 ] + ).toBeInTheDocument(); + } + } ); + + it( 'should render mediaField if defined', () => { + render( + <DataViewWrapper + view={ { + type: 'grid', + mediaField: 'image', + } } + /> + ); + for ( const item of data ) { + expect( + screen.getByTestId( 'image-field-' + item.id ) + ).toBeInTheDocument(); + } + } ); + + it( 'should render actions dropdown if actions are supported and passed in for each grid item', () => { + render( + <DataViewWrapper + view={ { + type: 'grid', + } } + actions={ actions } + /> + ); + expect( + screen.getAllByRole( 'button', { name: 'Actions' } ).length + ).toEqual( 3 ); + } ); + + it( 'should trigger the onClickItem callback if isItemClickable returns true and a media field is clicked', async () => { + const mediaClickItemCallback = jest.fn(); + + render( + <DataViewWrapper + view={ { + type: 'grid', + mediaField: 'image', + } } + actions={ actions } + isItemClickable={ () => true } + onClickItem={ mediaClickItemCallback } + /> + ); + const imageField = screen.getByTestId( + 'image-field-' + data[ 0 ].id + ); + const user = userEvent.setup(); + await user.click( imageField ); + expect( mediaClickItemCallback ).toHaveBeenCalledWith( data[ 0 ] ); + } ); + } ); + + describe( 'in list view', () => { + it( 'should display the passed in data', () => { + render( + <DataViewWrapper + view={ { + type: 'list', + } } + /> + ); + for ( const item of data ) { + expect( + screen.getAllByText( item.title )[ 0 ] + ).toBeInTheDocument(); + } + } ); + + it( 'should render actions dropdown if actions are supported and passed in for each list item', () => { + render( + <DataViewWrapper + view={ { + type: 'list', + } } + actions={ actions } + /> + ); + expect( + screen.getAllByRole( 'button', { name: 'Actions' } ).length + ).toEqual( 3 ); + } ); + } ); +} ); diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 96fd4a8cd01afc..820f75364df204 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -322,6 +322,11 @@ interface ViewBase { * Whether to show the description */ showDescription?: boolean; + + /** + * Whether to show the hierarchical levels. + */ + showLevels?: boolean; } export interface ColumnStyle { @@ -480,6 +485,7 @@ export interface ViewBaseProps< Item > { data: Item[]; fields: NormalizedField< Item >[]; getItemId: ( item: Item ) => string; + getItemLevel?: ( item: Item ) => number; isLoading?: boolean; onChangeView: ( view: View ) => void; onChangeSelection: SetSelection; diff --git a/packages/dataviews/tsconfig.json b/packages/dataviews/tsconfig.json index 78e68b5a7c98b4..a7c8759d257cb2 100644 --- a/packages/dataviews/tsconfig.json +++ b/packages/dataviews/tsconfig.json @@ -2,9 +2,12 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", - "checkJs": false + "types": [ + "gutenberg-env", + "gutenberg-test-env", + "jest", + "@testing-library/jest-dom" + ] }, "references": [ { "path": "../components" }, @@ -17,5 +20,12 @@ { "path": "../private-apis" }, { "path": "../warning" } ], - "include": [ "src" ] + "exclude": [ + "src/**/*.android.js", + "src/**/*.ios.js", + "src/**/*.native.js", + "src/**/react-native-*", + "src/**/stories/**/*.js", // only exclude js files, tsx files should be checked + "src/**/test/**/*.js" // only exclude js files, ts{x} files should be checked + ] } diff --git a/packages/date/tsconfig.json b/packages/date/tsconfig.json index 0c9e6d5ed02b0b..605262dd7cc952 100644 --- a/packages/date/tsconfig.json +++ b/packages/date/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "references": [ { "path": "../deprecated" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../deprecated" } ] } diff --git a/packages/deprecated/tsconfig.json b/packages/deprecated/tsconfig.json index f90e327f124d7e..b2186db14f4cc4 100644 --- a/packages/deprecated/tsconfig.json +++ b/packages/deprecated/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "references": [ { "path": "../hooks" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../hooks" } ] } diff --git a/packages/docgen/tsconfig.json b/packages/docgen/tsconfig.json index df0072645c53ba..eebc743289aec2 100644 --- a/packages/docgen/tsconfig.json +++ b/packages/docgen/tsconfig.json @@ -2,8 +2,8 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "lib", - "declarationDir": "build-types" + "rootDir": "lib" }, - "include": [ "lib/get-leading-comments.js" ] + "files": [ "lib/get-leading-comments.js" ], + "include": [] } diff --git a/packages/dom-ready/tsconfig.json b/packages/dom-ready/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/dom-ready/tsconfig.json +++ b/packages/dom-ready/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/dom/src/dom/clean-node-list.js b/packages/dom/src/dom/clean-node-list.js index bbdff13b470d69..f1b7e1be7264f4 100644 --- a/packages/dom/src/dom/clean-node-list.js +++ b/packages/dom/src/dom/clean-node-list.js @@ -76,7 +76,10 @@ export default function cleanNodeList( nodeList, doc, schema, inline ) { // TODO: Explore patching this in jsdom-jscore. if ( node.classList && node.classList.length ) { const mattchers = classes.map( ( item ) => { - if ( typeof item === 'string' ) { + if ( item === '*' ) { + // Keep all classes. + return () => true; + } else if ( typeof item === 'string' ) { return ( /** @type {string} */ className ) => className === item; diff --git a/packages/dom/tsconfig.json b/packages/dom/tsconfig.json index 7cdff6c141151b..e44d6b98c50856 100644 --- a/packages/dom/tsconfig.json +++ b/packages/dom/tsconfig.json @@ -2,10 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ] }, - "include": [ "src/**/*" ], "references": [ { "path": "../deprecated" } ] } diff --git a/packages/e2e-test-utils-playwright/tsconfig.json b/packages/e2e-test-utils-playwright/tsconfig.json index 5e52bb94f706d9..947a4a0f82fc76 100644 --- a/packages/e2e-test-utils-playwright/tsconfig.json +++ b/packages/e2e-test-utils-playwright/tsconfig.json @@ -7,16 +7,13 @@ "module": "Node16", "moduleResolution": "node16", "types": [ "node" ], - "rootDir": "src", "noEmit": false, "outDir": "build", "sourceMap": true, "declaration": true, "declarationMap": true, - "declarationDir": "build-types", "emitDeclarationOnly": false, "allowJs": true, "checkJs": false - }, - "include": [ "src/**/*" ] + } } diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md index 30548961db26a4..5d445b2697d5ef 100644 --- a/packages/e2e-test-utils/README.md +++ b/packages/e2e-test-utils/README.md @@ -212,7 +212,7 @@ Create a new user account. _Parameters_ - _username_ `string`: User name. -- _object_ `Object?`: Optional Settings for the new user account. +- _object_ `?Object`: Optional Settings for the new user account. - _object.firstName_ `[string]`: First name. - _object.lastName_ `[string]`: Last name. - _object.role_ `[string]`: Role. Defaults to Administrator. @@ -252,7 +252,7 @@ Deletes a theme from the site, activating another theme if necessary. _Parameters_ - _slug_ `string`: Theme slug. -- _settings_ `Object?`: Optional settings object. +- _settings_ `?Object`: Optional settings object. - _settings.newThemeSlug_ `?string`: A theme to switch to if the theme to delete is active. Required if the theme to delete is active. - _settings.newThemeSearchTerm_ `?string`: A search term to use if the new theme is not findable by its slug. @@ -488,7 +488,7 @@ Installs a theme from the WP.org repository. _Parameters_ - _slug_ `string`: Theme slug. -- _settings_ `Object?`: Optional settings object. +- _settings_ `?Object`: Optional settings object. - _settings.searchTerm_ `?string`: Search term to use if the theme is not findable by its slug. ### isCurrentURL diff --git a/packages/e2e-test-utils/src/create-user.js b/packages/e2e-test-utils/src/create-user.js index 317f23c4c58d40..ac28a59482e381 100644 --- a/packages/e2e-test-utils/src/create-user.js +++ b/packages/e2e-test-utils/src/create-user.js @@ -14,7 +14,7 @@ import { visitAdminPage } from './visit-admin-page'; * Create a new user account. * * @param {string} username User name. - * @param {Object?} object Optional Settings for the new user account. + * @param {?Object} object Optional Settings for the new user account. * @param {string} [object.firstName] First name. * @param {string} [object.lastName] Last name. * @param {string} [object.role] Role. Defaults to Administrator. diff --git a/packages/e2e-test-utils/src/delete-theme.js b/packages/e2e-test-utils/src/delete-theme.js index 8b59c9f1e7a112..b09bc6424b99bd 100644 --- a/packages/e2e-test-utils/src/delete-theme.js +++ b/packages/e2e-test-utils/src/delete-theme.js @@ -12,7 +12,7 @@ import { isThemeInstalled } from './theme-installed'; * Deletes a theme from the site, activating another theme if necessary. * * @param {string} slug Theme slug. - * @param {Object?} settings Optional settings object. + * @param {?Object} settings Optional settings object. * @param {?string} settings.newThemeSlug A theme to switch to if the theme to delete is active. Required if the theme to delete is active. * @param {?string} settings.newThemeSearchTerm A search term to use if the new theme is not findable by its slug. */ diff --git a/packages/e2e-test-utils/src/install-theme.js b/packages/e2e-test-utils/src/install-theme.js index 7f11e5da88ef83..8adf7fe58a20cf 100644 --- a/packages/e2e-test-utils/src/install-theme.js +++ b/packages/e2e-test-utils/src/install-theme.js @@ -10,7 +10,7 @@ import { isThemeInstalled } from './theme-installed'; * Installs a theme from the WP.org repository. * * @param {string} slug Theme slug. - * @param {Object?} settings Optional settings object. + * @param {?Object} settings Optional settings object. * @param {?string} settings.searchTerm Search term to use if the theme is not findable by its slug. */ export async function installTheme( slug, { searchTerm } = {} ) { diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php index b86673c2c523d0..1fd6d8468c77db 100644 --- a/packages/e2e-tests/plugins/block-bindings.php +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -41,7 +41,11 @@ function gutenberg_test_block_bindings_registration() { plugins_url( 'block-bindings/index.js', __FILE__ ), array( 'wp-blocks', - 'wp-private-apis', + 'wp-block-editor', + 'wp-components', + 'wp-compose', + 'wp-element', + 'wp-hooks', ), filemtime( plugin_dir_path( __FILE__ ) . 'block-bindings/index.js' ), true diff --git a/packages/e2e-tests/plugins/block-bindings/index.js b/packages/e2e-tests/plugins/block-bindings/index.js index 5c364257caed19..63c463e197fa8a 100644 --- a/packages/e2e-tests/plugins/block-bindings/index.js +++ b/packages/e2e-tests/plugins/block-bindings/index.js @@ -1,4 +1,9 @@ const { registerBlockBindingsSource } = wp.blocks; +const { InspectorControls } = wp.blockEditor; +const { PanelBody, TextControl } = wp.components; +const { createHigherOrderComponent } = wp.compose; +const { createElement: el, Fragment } = wp.element; +const { addFilter } = wp.hooks; const { fieldsList } = window.testingBindings || {}; const getValues = ( { bindings } ) => { @@ -46,3 +51,43 @@ registerBlockBindingsSource( { getValues, canUserEditValue: () => true, } ); + +const withBlockBindingsInspectorControl = createHigherOrderComponent( + ( BlockEdit ) => { + return ( props ) => { + if ( ! props.attributes?.metadata?.bindings?.content ) { + return el( BlockEdit, props ); + } + + return el( + Fragment, + {}, + el( BlockEdit, props ), + el( + InspectorControls, + {}, + el( + PanelBody, + { title: 'Bindings' }, + el( TextControl, { + __next40pxDefaultSize: true, + __nextHasNoMarginBottom: true, + label: 'Content', + value: props.attributes.content, + onChange: ( content ) => + props.setAttributes( { + content, + } ), + } ) + ) + ) + ); + }; + } +); + +addFilter( + 'editor.BlockEdit', + 'testing/bindings-inspector-control', + withBlockBindingsInspectorControl +); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php index 47eb351d837e78..bfac62feb13595 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/render.php @@ -260,3 +260,54 @@ data-wp-text="context.callbackRunCount" ></data> </div> + +<hr> + +<div + data-wp-interactive="directive-each" + data-testid="each-with-unset" +> + <template data-wp-each="state.eachUnset"><p data-wp-text="context.item"></p></template> +</div> +<div + data-wp-interactive="directive-each" + data-testid="each-with-null" +> + <template data-wp-each="state.eachNull"><p data-wp-text="context.item"></p></template> +</div> +<div + data-wp-interactive="directive-each" + data-testid="each-with-undefined" +> + <template data-wp-each="state.eachUndefined"><p data-wp-text="context.item"></p></template> +</div> +<div + data-wp-interactive="directive-each" + data-testid="each-with-array" +> + <template data-wp-each="state.eachArray"><p data-wp-text="context.item"></p></template> +</div> +<div + data-wp-interactive="directive-each" + data-testid="each-with-set" +> + <template data-wp-each="state.eachSet"><p data-wp-text="context.item"></p></template> +</div> +<div + data-wp-interactive="directive-each" + data-testid="each-with-string" +> + <template data-wp-each="state.eachString"><p data-wp-text="context.item"></p></template> +</div> +<div + data-wp-interactive="directive-each" + data-testid="each-with-generator" +> + <template data-wp-each="state.eachGenerator"><p data-wp-text="context.item"></p></template> +</div> +<div + data-wp-interactive="directive-each" + data-testid="each-with-iterator" +> + <template data-wp-each="state.eachIterator"><p data-wp-text="context.item"></p></template> +</div> diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js index 6ceef82864d9db..7577810b6bb876 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-each/view.js @@ -13,6 +13,28 @@ const { state } = store( 'directive-each' ); store( 'directive-each', { state: { letters: [ 'A', 'B', 'C' ], + eachUndefined: undefined, + eachNull: null, + eachArray: [ 'an', 'array' ], + eachSet: new Set( [ 'a', 'set' ] ), + eachString: 'str', + *eachGenerator() { + yield 'a'; + yield 'generator'; + }, + eachIterator: { + [ Symbol.iterator ]() { + const vals = [ 'implements', 'iterator' ]; + let i = 0; + return { + next() { + return i < vals.length + ? { value: vals[ i++ ], done: false } + : { done: true }; + }, + }; + }, + }, }, } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/assets/10x10_e2e_test_image_blue.png b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/assets/10x10_e2e_test_image_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..c4f8e7c5146d362a505607042039b832115b9dfb GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVk{1FcVfJGQl}os;VkfoEM{Qf z76xHPhFNnYfP%~cJ|V6Q4FCStKRO8H@_M>BhE&W+PDw~O(#ybbn1TC*g4kN11cRrm KpUXO@geCyWY#fvT literal 0 HcmV?d00001 diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/block.json new file mode 100644 index 00000000000000..644ea70f74dca1 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/router-styles-blue", + "title": "E2E Interactivity tests - router styles - Blue", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewStyle": "file:./style.css", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/render.php new file mode 100644 index 00000000000000..3f5da308db092a --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/render.php @@ -0,0 +1,35 @@ +<?php +/** + * HTML for testing the iAPI's style assets management. + * + * @package gutenberg-test-interactive-blocks + * + * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable + */ + +add_action( + 'wp_enqueue_scripts', + function () { + wp_enqueue_style( + 'blue-from-link', + plugin_dir_url( __FILE__ ) . 'style-from-link.css', + array() + ); + + $custom_css = ' + .blue-from-inline { + color: rgb(0, 0, 255); + } + '; + + wp_register_style( 'test-router-styles', false ); + wp_enqueue_style( 'test-router-styles' ); + wp_add_inline_style( 'test-router-styles', $custom_css ); + } +); + +$wrapper_attributes = get_block_wrapper_attributes( + array( 'data-testid' => 'blue-block' ) +); +?> +<p <?php echo $wrapper_attributes; ?>>Blue</p> diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style-from-link.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style-from-link.css new file mode 100644 index 00000000000000..f55f12f4d594cf --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style-from-link.css @@ -0,0 +1,7 @@ +.blue-from-link { + color: rgb(0, 0, 255); +} + +.background-from-link { + background-image: url('./assets/10x10_e2e_test_image_blue.png'); +} \ No newline at end of file diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style.css new file mode 100644 index 00000000000000..84d891e90242a5 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-blue/style.css @@ -0,0 +1,4 @@ +.wp-block-test-router-styles-blue, +.blue { + color: rgb(0, 0, 255); +} \ No newline at end of file diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/assets/10x10_e2e_test_image_green.png b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/assets/10x10_e2e_test_image_green.png new file mode 100644 index 0000000000000000000000000000000000000000..34ec87925d8c5045f71f6d09724db8fa00ff03af GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVk{1FcVfJGQl}os;VkfoEM{Qf z76xHPhFNnYfP%~cJ|V6Q{~1i;4y*ujc|BbmLn`JZrz9jC>1ALz%)os@L2NBhg2B_( K&t;ucLK6U;SQ~Hv literal 0 HcmV?d00001 diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/block.json new file mode 100644 index 00000000000000..e2edda625571b9 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/router-styles-green", + "title": "E2E Interactivity tests - router styles - Green", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewStyle": "file:./style.css", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/render.php new file mode 100644 index 00000000000000..4418a2d3ab0f3d --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/render.php @@ -0,0 +1,35 @@ +<?php +/** + * HTML for testing the iAPI's style assets management. + * + * @package gutenberg-test-interactive-blocks + * + * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable + */ + +add_action( + 'wp_enqueue_scripts', + function () { + wp_enqueue_style( + 'green-from-link', + plugin_dir_url( __FILE__ ) . 'style-from-link.css', + array() + ); + + $custom_css = ' + .green-from-inline { + color: rgb(0, 255, 0); + } + '; + + wp_register_style( 'test-router-styles', false ); + wp_enqueue_style( 'test-router-styles' ); + wp_add_inline_style( 'test-router-styles', $custom_css ); + } +); + +$wrapper_attributes = get_block_wrapper_attributes( + array( 'data-testid' => 'green-block' ) +); +?> +<p <?php echo $wrapper_attributes; ?>>Green</p> diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style-from-link.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style-from-link.css new file mode 100644 index 00000000000000..b3d7d7b111e52a --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style-from-link.css @@ -0,0 +1,7 @@ +.green-from-link { + color: rgb(0, 255, 0); +} + +.background-from-link { + background-image: url('./assets/10x10_e2e_test_image_green.png'); +} \ No newline at end of file diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style.css new file mode 100644 index 00000000000000..0c457588f625cb --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-green/style.css @@ -0,0 +1,4 @@ +.wp-block-test-router-styles-green, +.green { + color: rgb(0, 255, 0); +} \ No newline at end of file diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/assets/10x10_e2e_test_image_red.png b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/assets/10x10_e2e_test_image_red.png new file mode 100644 index 0000000000000000000000000000000000000000..3264bf6427c2766e0b2e6cb8c5dc0f88d123e239 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2xGmzZ=C-xtZVk{1FcVfJGQl}os;VkfoEM{Qf z76xHPhFNnYfP%~cJ|V9E85krVahe0Uyq+$OAr*6yQxX!6^fE9UX5c=dAhs4L!QkoY K=d#Wzp$Pzi{Ti_V literal 0 HcmV?d00001 diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/block.json new file mode 100644 index 00000000000000..582d7019062c6e --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/router-styles-red", + "title": "E2E Interactivity tests - router styles - Red", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewStyle": "file:./style.css", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/render.php new file mode 100644 index 00000000000000..e8474cf69b825a --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/render.php @@ -0,0 +1,35 @@ +<?php +/** + * HTML for testing the iAPI's style assets management. + * + * @package gutenberg-test-interactive-blocks + * + * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable + */ + +add_action( + 'wp_enqueue_scripts', + function () { + wp_enqueue_style( + 'red-from-link', + plugin_dir_url( __FILE__ ) . 'style-from-link.css', + array() + ); + + $custom_css = ' + .red-from-inline { + color: rgb(255, 0, 0); + } + '; + + wp_register_style( 'test-router-styles', false ); + wp_enqueue_style( 'test-router-styles' ); + wp_add_inline_style( 'test-router-styles', $custom_css ); + } +); + +$wrapper_attributes = get_block_wrapper_attributes( + array( 'data-testid' => 'red-block' ) +); +?> +<p <?php echo $wrapper_attributes; ?>>Red</p> diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style-from-link.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style-from-link.css new file mode 100644 index 00000000000000..0f7d6228079897 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style-from-link.css @@ -0,0 +1,7 @@ +.red-from-link { + color: rgb(255, 0, 0); +} + +.background-from-link { + background-image: url('./assets/10x10_e2e_test_image_red.png'); +} \ No newline at end of file diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style.css new file mode 100644 index 00000000000000..eac7e3af16e0b5 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-red/style.css @@ -0,0 +1,4 @@ +.wp-block-test-router-styles-red, +.red { + color: rgb(255, 0, 0); +} \ No newline at end of file diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/block.json new file mode 100644 index 00000000000000..a1a95b4c81e3b6 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/block.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/router-styles-wrapper", + "title": "E2E Interactivity tests - router styles - Wrapper", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScriptModule": "file:./view.js", + "viewStyle": "file:./style.css", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php new file mode 100644 index 00000000000000..6373e8e9bc235b --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/render.php @@ -0,0 +1,70 @@ +<?php +/** + * HTML for testing the iAPI's style assets management. + * + * @package gutenberg-test-interactive-blocks + * + * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable + */ + +$wrapper_attributes = get_block_wrapper_attributes(); +?> +<div <?php echo $wrapper_attributes; ?>> + <!-- These get colored when the corresponding block is present. --> + <fieldset> + <legend>Styles from block styles</legend> + <p data-testid="red" class="red">Red</p> + <p data-testid="green" class="green">Green</p> + <p data-testid="blue" class="blue">Blue</p> + <p data-testid="all" class="red green blue">All</p> + </fieldset> + + <!-- These get colored when the corresponding block enqueues a referenced stylesheet. --> + <fieldset> + <legend>Styles from referenced style sheets</legend> + <p data-testid="red-from-link" class="red-from-link">Red from link</p> + <p data-testid="green-from-link" class="green-from-link">Green from link</p> + <p data-testid="blue-from-link" class="blue-from-link">Blue from link</p> + <p data-testid="all-from-link" class="red-from-link green-from-link blue-from-link">All from link</p> + <div data-testid="background-from-link"class="background-from-link" style="width: 10px; height: 10px"></div> + </fieldset> + + <!-- These get colored when the corresponding block adds inline style. --> + <fieldset> + <legend>Styles from inline styles</legend> + <p data-testid="red-from-inline" class="red-from-inline">Red</p> + <p data-testid="green-from-inline" class="green-from-inline">Green</p> + <p data-testid="blue-from-inline" class="blue-from-inline">Blue</p> + <p data-testid="all-from-inline" class="red-from-inline green-from-inline blue-from-inline">All</p> + </fieldset> + + <!-- Links to pages with different blocks combination. --> + <nav data-wp-interactive="test/router-styles"> + <?php foreach ( $attributes['links'] as $label => $link ) : ?> + <a + data-testid="link <?php echo $label; ?>" + data-wp-on--click="actions.navigate" + href="<?php echo $link; ?>" + > + <?php echo $label; ?> + </a> + <?php endforeach; ?> + </nav> + + <!-- HTML updated on navigation. --> + <div + data-wp-interactive="test/router-styles" + data-wp-router-region="router-styles" + > + <?php echo $content; ?> + </div> + + <!-- Text to check whether a navigation was client-side. --> + <div + data-testid="client-side navigation" + data-wp-interactive="test/router-styles" + data-wp-bind--hidden="!state.clientSideNavigation" + > + Client-side navigation + </div> +</div> diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/style.css b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/style.css new file mode 100644 index 00000000000000..12773560c4180f --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/style.css @@ -0,0 +1,3 @@ +.wp-block-test-router-styles-wrapper { + color: rgb(160, 12, 60); +} \ No newline at end of file diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.asset.php new file mode 100644 index 00000000000000..bdaec8d1b67a9d --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.asset.php @@ -0,0 +1,9 @@ +<?php return array( + 'dependencies' => array( + '@wordpress/interactivity', + array( + 'id' => '@wordpress/interactivity-router', + 'import' => 'dynamic', + ), + ), +); diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js new file mode 100644 index 00000000000000..5b3b42f2b413e4 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-styles-wrapper/view.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { store } from '@wordpress/interactivity'; + +const { state } = store( 'test/router-styles', { + state: { + clientSideNavigation: false, + }, + actions: { + *navigate( e ) { + e.preventDefault(); + const { actions } = yield import( + '@wordpress/interactivity-router' + ); + yield actions.navigate( e.target.href ); + state.clientSideNavigation = true; + }, + }, +} ); diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index b0a2b3f7d76b81..acc71afe1db0a8 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -318,7 +318,9 @@ function MetaBoxesMain() { // the event to end the drag is captured by the target (resize handle) // whether or not it’s under the pointer. onPointerDown: ( { pointerId, target } ) => { - target.setPointerCapture( pointerId ); + if ( separatorRef.current.parentElement.contains( target ) ) { + target.setPointerCapture( pointerId ); + } }, onResizeStart: ( event, direction, elementRef ) => { if ( isAutoHeight ) { @@ -405,6 +407,9 @@ function Layout( { const isRenderingPostOnly = getRenderingMode() === 'post-only'; const isNotDesignPostType = ! DESIGN_POST_TYPES.includes( currentPostType ); + const isDirectlyEditingPattern = + currentPostType === 'wp_block' && + ! onNavigateToPreviousEntityRecord; return { mode: getEditorMode(), @@ -415,7 +420,9 @@ function Layout( { !! select( blockEditorStore ).getBlockSelectionStart(), showIconLabels: get( 'core', 'showIconLabels' ), isDistractionFree: get( 'core', 'distractionFree' ), - showMetaBoxes: isNotDesignPostType && ! isZoomOut(), + showMetaBoxes: + ( isNotDesignPostType && ! isZoomOut() ) || + isDirectlyEditingPattern, isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), templateId: supportsTemplateMode && @@ -433,6 +440,7 @@ function Layout( { currentPostId, isEditingTemplate, settings.supportsTemplateMode, + onNavigateToPreviousEntityRecord, ] ); useMetaBoxInitialization( hasActiveMetaboxes ); diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index f6516dd0206c00..58c802f579e0d1 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -550,7 +550,7 @@ export function areMetaBoxesInitialized( state ) { /** * Retrieves the template of the currently edited post. * - * @return {Object?} Post Template. + * @return {?Object} Post Template. */ export const getEditedPostTemplate = createRegistrySelector( ( select ) => () => { diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 51045be503de31..299f0a67da9b7a 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -56,6 +56,7 @@ "@wordpress/icons": "*", "@wordpress/keyboard-shortcuts": "*", "@wordpress/keycodes": "*", + "@wordpress/media-utils": "5.14.0", "@wordpress/notices": "*", "@wordpress/patterns": "*", "@wordpress/plugins": "*", diff --git a/packages/edit-site/src/components/add-new-pattern/index.js b/packages/edit-site/src/components/add-new-pattern/index.js index 63452691c1c373..85a8c70f9c3359 100644 --- a/packages/edit-site/src/components/add-new-pattern/index.js +++ b/packages/edit-site/src/components/add-new-pattern/index.js @@ -25,7 +25,7 @@ import { TEMPLATE_PART_POST_TYPE, } from '../../utils/constants'; -const { useHistory } = unlock( routerPrivateApis ); +const { useHistory, useLocation } = unlock( routerPrivateApis ); const { CreatePatternModal, useAddPatternCategory } = unlock( editPatternsPrivateApis ); @@ -33,6 +33,7 @@ const { CreateTemplatePartModal } = unlock( editorPrivateApis ); export default function AddNewPattern() { const history = useHistory(); + const location = useLocation(); const [ showPatternModal, setShowPatternModal ] = useState( false ); const [ showTemplatePartModal, setShowTemplatePartModal ] = useState( false ); @@ -159,13 +160,12 @@ export default function AddNewPattern() { return; } try { - const { - params: { postType, categoryId }, - } = history.getLocationWithParams(); let currentCategoryId; // When we're not handling template parts, we should // add or create the proper pattern category. - if ( postType !== TEMPLATE_PART_POST_TYPE ) { + if ( + location.query.postType !== TEMPLATE_PART_POST_TYPE + ) { /* * categoryMap.values() returns an iterator. * Iterator.prototype.find() is not yet widely supported. @@ -173,7 +173,10 @@ export default function AddNewPattern() { */ const currentCategory = Array.from( categoryMap.values() - ).find( ( term ) => term.name === categoryId ); + ).find( + ( term ) => + term.name === location.query.categoryId + ); if ( currentCategory ) { currentCategoryId = currentCategory.id || @@ -194,7 +197,7 @@ export default function AddNewPattern() { // category. if ( ! currentCategoryId && - categoryId !== 'my-patterns' + location.query.categoryId !== 'my-patterns' ) { history.navigate( `/pattern?categoryId=${ PATTERN_DEFAULT_CATEGORY }` diff --git a/packages/edit-site/src/components/canvas-loader/style.scss b/packages/edit-site/src/components/canvas-loader/style.scss index 3d74d408aeceda..33ff6dc38c3f51 100644 --- a/packages/edit-site/src/components/canvas-loader/style.scss +++ b/packages/edit-site/src/components/canvas-loader/style.scss @@ -9,9 +9,10 @@ align-items: center; justify-content: center; - animation: 0.5s ease 0.2s edit-site-canvas-loader__fade-in-animation; - animation-fill-mode: forwards; - @include reduce-motion("animation"); + @media not (prefers-reduced-motion) { + animation: 0.5s ease 0.2s edit-site-canvas-loader__fade-in-animation; + animation-fill-mode: forwards; + } & > div { width: 160px; diff --git a/packages/edit-site/src/components/editor-canvas-container/style.scss b/packages/edit-site/src/components/editor-canvas-container/style.scss index 52ac29da0696f6..c544f88f6bcd58 100644 --- a/packages/edit-site/src/components/editor-canvas-container/style.scss +++ b/packages/edit-site/src/components/editor-canvas-container/style.scss @@ -1,6 +1,10 @@ .edit-site-editor-canvas-container { height: 100%; + // This is the gray background color that's applied behind "isolation mode". + // The color normally comes from .editor-visual-editor, but that class is missing here. + background-color: $gray-300; + // Controls height of editor and editor canvas container (style book, global styles revisions previews etc.) iframe { display: block; @@ -22,7 +26,9 @@ position: absolute; right: 0; top: 0; - transition: all 0.3s; // Match .block-editor-iframe__body transition. + @media not (prefers-reduced-motion) { + transition: all 0.3s; // Match .block-editor-iframe__body transition. + } } .edit-site-editor-canvas-container__close-button { diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index c045bafd8a6839..ad88ee07e2150f 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -20,7 +20,6 @@ import { privateApis as blockLibraryPrivateApis } from '@wordpress/block-library import { useCallback, useMemo } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { store as preferencesStore } from '@wordpress/preferences'; import { decodeEntities } from '@wordpress/html-entities'; import { Icon, arrowUpLeft } from '@wordpress/icons'; import { store as blockEditorStore } from '@wordpress/block-editor'; @@ -130,7 +129,6 @@ export default function EditSiteEditor( { isPostsList = false } ) { const { postType, postId, context } = entity; const { supportsGlobalStyles, - showIconLabels, editorCanvasView, currentPostIsTrashed, hasSiteIcon, @@ -138,13 +136,11 @@ export default function EditSiteEditor( { isPostsList = false } ) { const { getEditorCanvasContainerView } = unlock( select( editSiteStore ) ); - const { get } = select( preferencesStore ); const { getCurrentTheme, getEntityRecord } = select( coreDataStore ); const siteData = getEntityRecord( 'root', '__unstableBase', undefined ); return { supportsGlobalStyles: getCurrentTheme()?.is_block_theme, - showIconLabels: get( 'core', 'showIconLabels' ), editorCanvasView: getEditorCanvasContainerView(), currentPostIsTrashed: select( editorStore ).getCurrentPostAttribute( 'status' ) === @@ -267,9 +263,7 @@ export default function EditSiteEditor( { isPostsList = false } ) { postId={ postWithTemplate ? context.postId : postId } templateId={ postWithTemplate ? postId : undefined } settings={ settings } - className={ clsx( 'edit-site-editor__editor-interface', { - 'show-icon-labels': showIconLabels, - } ) } + className="edit-site-editor__editor-interface" styles={ styles } customSaveButton={ _isPreviewingTheme && <SaveButton size="compact" /> diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss index a6cc5084966947..625b2633ab7244 100644 --- a/packages/edit-site/src/components/editor/style.scss +++ b/packages/edit-site/src/components/editor/style.scss @@ -1,7 +1,9 @@ .edit-site-editor__editor-interface { opacity: 1; - transition: opacity 0.1s ease-out; - @include reduce-motion( "transition" ); + + @media not (prefers-reduced-motion) { + transition: opacity 0.1s ease-out; + } &.is-loading { opacity: 0; diff --git a/packages/edit-site/src/components/global-styles/confirm-reset-shadow-dialog.js b/packages/edit-site/src/components/global-styles/confirm-reset-shadow-dialog.js new file mode 100644 index 00000000000000..b8f5b77010ff6d --- /dev/null +++ b/packages/edit-site/src/components/global-styles/confirm-reset-shadow-dialog.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +function ConfirmResetShadowDialog( { + text, + confirmButtonText, + isOpen, + toggleOpen, + onConfirm, +} ) { + const handleConfirm = async () => { + toggleOpen(); + onConfirm(); + }; + + const handleCancel = () => { + toggleOpen(); + }; + + return ( + <ConfirmDialog + isOpen={ isOpen } + cancelButtonText={ __( 'Cancel' ) } + confirmButtonText={ confirmButtonText } + onCancel={ handleCancel } + onConfirm={ handleConfirm } + size="medium" + > + { text } + </ConfirmDialog> + ); +} + +export default ConfirmResetShadowDialog; 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 27093e0ef1cbba..5661a002f71ecb 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 @@ -28,7 +28,7 @@ const DEFAULT_TAB = { const UPLOAD_TAB = { id: 'upload-fonts', - title: __( 'Upload' ), + title: _x( 'Upload', 'noun' ), }; const tabsFromCollections = ( collections ) => 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 7568fea2b6f805..11a1c6d6689370 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 @@ -129,8 +129,10 @@ $footer-height: 70px; .font-library-modal__font-variant_demo-text { white-space: nowrap; flex-shrink: 0; - transition: opacity 0.3s ease-in-out; - @include reduce-motion( "transition" ); + + @media not (prefers-reduced-motion) { + transition: opacity 0.3s ease-in-out; + } } } diff --git a/packages/edit-site/src/components/global-styles/font-sizes/font-size.js b/packages/edit-site/src/components/global-styles/font-sizes/font-size.js index 25dcc69185cae6..cca4a26e1b7368 100644 --- a/packages/edit-site/src/components/global-styles/font-sizes/font-size.js +++ b/packages/edit-site/src/components/global-styles/font-sizes/font-size.js @@ -166,25 +166,34 @@ function FontSize() { marginBottom={ 0 } paddingX={ 4 } > - <Menu - trigger={ - <Button - size="small" - icon={ moreVertical } - label={ __( 'Font size options' ) } - /> - } - > - <Menu.Item onClick={ toggleRenameDialog }> - <Menu.ItemLabel> - { __( 'Rename' ) } - </Menu.ItemLabel> - </Menu.Item> - <Menu.Item onClick={ toggleDeleteConfirm }> - <Menu.ItemLabel> - { __( 'Delete' ) } - </Menu.ItemLabel> - </Menu.Item> + <Menu> + <Menu.TriggerButton + render={ + <Button + size="small" + icon={ moreVertical } + label={ __( + 'Font size options' + ) } + /> + } + /> + <Menu.Popover> + <Menu.Item + onClick={ toggleRenameDialog } + > + <Menu.ItemLabel> + { __( 'Rename' ) } + </Menu.ItemLabel> + </Menu.Item> + <Menu.Item + onClick={ toggleDeleteConfirm } + > + <Menu.ItemLabel> + { __( 'Delete' ) } + </Menu.ItemLabel> + </Menu.Item> + </Menu.Popover> </Menu> </Spacer> </FlexItem> diff --git a/packages/edit-site/src/components/global-styles/font-sizes/font-sizes.js b/packages/edit-site/src/components/global-styles/font-sizes/font-sizes.js index 4bda7a7b3266b5..5b759d1e0468d8 100644 --- a/packages/edit-site/src/components/global-styles/font-sizes/font-sizes.js +++ b/packages/edit-site/src/components/global-styles/font-sizes/font-sizes.js @@ -11,7 +11,6 @@ import { __experimentalVStack as VStack, __experimentalHStack as HStack, FlexItem, - FlexBlock, Button, } from '@wordpress/components'; import { @@ -27,14 +26,15 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import { unlock } from '../../../lock-unlock'; -const { Menu } = unlock( componentsPrivateApis ); -const { useGlobalSetting } = unlock( blockEditorPrivateApis ); import Subtitle from '../subtitle'; import { NavigationButtonAsItem } from '../navigation-button'; import { getNewIndexFromPresets } from '../utils'; import ScreenHeader from '../header'; import ConfirmResetFontSizesDialog from './confirm-reset-font-sizes-dialog'; +const { Menu } = unlock( componentsPrivateApis ); +const { useGlobalSetting } = unlock( blockEditorPrivateApis ); + function FontSizeGroup( { label, origin, @@ -81,24 +81,31 @@ function FontSizeGroup( { /> ) } { !! handleResetFontSizes && ( - <Menu - trigger={ - <Button - size="small" - icon={ moreVertical } - label={ __( - 'Font size presets options' - ) } - /> - } - > - <Menu.Item onClick={ toggleResetDialog }> - <Menu.ItemLabel> - { origin === 'custom' - ? __( 'Remove font size presets' ) - : __( 'Reset font size presets' ) } - </Menu.ItemLabel> - </Menu.Item> + <Menu> + <Menu.TriggerButton + render={ + <Button + size="small" + icon={ moreVertical } + label={ __( + 'Font size presets options' + ) } + /> + } + /> + <Menu.Popover> + <Menu.Item onClick={ toggleResetDialog }> + <Menu.ItemLabel> + { origin === 'custom' + ? __( + 'Remove font size presets' + ) + : __( + 'Reset font size presets' + ) } + </Menu.ItemLabel> + </Menu.Item> + </Menu.Popover> </Menu> ) } </FlexItem> @@ -111,23 +118,18 @@ function FontSizeGroup( { key={ size.slug } path={ `/typography/font-sizes/${ origin }/${ size.slug }` } > - <HStack direction="row"> + <HStack> <FlexItem className="edit-site-font-size__item"> { size.name } </FlexItem> - <FlexItem> - <HStack justify="flex-end"> - <FlexBlock className="edit-site-font-size__item edit-site-font-size__item-value"> - { size.size } - </FlexBlock> - <Icon - icon={ - isRTL() - ? chevronLeft - : chevronRight - } - /> - </HStack> + <FlexItem display="flex"> + <Icon + icon={ + isRTL() + ? chevronLeft + : chevronRight + } + /> </FlexItem> </HStack> </NavigationButtonAsItem> diff --git a/packages/edit-site/src/components/global-styles/screen-block.js b/packages/edit-site/src/components/global-styles/screen-block.js index 347d3cd1bc0a73..64f49574b6b03b 100644 --- a/packages/edit-site/src/components/global-styles/screen-block.js +++ b/packages/edit-site/src/components/global-styles/screen-block.js @@ -113,8 +113,9 @@ function ScreenBlock( { name, variation } ) { if ( settingsForBlockElement?.spacing?.blockGap && blockType?.supports?.spacing?.blockGap && - ( blockType?.supports?.spacing?.skipSerialization === true || - blockType?.supports?.spacing?.skipSerialization?.some?.( + ( blockType?.supports?.spacing?.__experimentalSkipSerialization === + true || + blockType?.supports?.spacing?.__experimentalSkipSerialization?.some?.( ( spacingType ) => spacingType === 'blockGap' ) ) ) { diff --git a/packages/edit-site/src/components/global-styles/shadows-edit-panel.js b/packages/edit-site/src/components/global-styles/shadows-edit-panel.js index 0de1f2c99362ca..93c6fe5751327e 100644 --- a/packages/edit-site/src/components/global-styles/shadows-edit-panel.js +++ b/packages/edit-site/src/components/global-styles/shadows-edit-panel.js @@ -163,33 +163,38 @@ export default function ShadowsEditPanel() { <ScreenHeader title={ selectedShadow.name } /> <FlexItem> <Spacer marginTop={ 2 } marginBottom={ 0 } paddingX={ 4 }> - <Menu - trigger={ - <Button - size="small" - icon={ moreVertical } - label={ __( 'Menu' ) } - /> - } - > - { ( category === 'custom' - ? customShadowMenuItems - : presetShadowMenuItems - ).map( ( item ) => ( - <Menu.Item - key={ item.action } - onClick={ () => onMenuClick( item.action ) } - disabled={ - item.action === 'reset' && - selectedShadow.shadow === - baseSelectedShadow.shadow - } - > - <Menu.ItemLabel> - { item.label } - </Menu.ItemLabel> - </Menu.Item> - ) ) } + <Menu> + <Menu.TriggerButton + render={ + <Button + size="small" + icon={ moreVertical } + label={ __( 'Menu' ) } + /> + } + /> + <Menu.Popover> + { ( category === 'custom' + ? customShadowMenuItems + : presetShadowMenuItems + ).map( ( item ) => ( + <Menu.Item + key={ item.action } + onClick={ () => + onMenuClick( item.action ) + } + disabled={ + item.action === 'reset' && + selectedShadow.shadow === + baseSelectedShadow.shadow + } + > + <Menu.ItemLabel> + { item.label } + </Menu.ItemLabel> + </Menu.Item> + ) ) } + </Menu.Popover> </Menu> </Spacer> </FlexItem> @@ -387,7 +392,12 @@ function ShadowItem( { shadow, onChange, canRemove, onRemove } ) { 'aria-expanded': isOpen, }; const removeButtonProps = { - onClick: onRemove, + onClick: () => { + if ( isOpen ) { + onToggle(); + } + onRemove(); + }, className: clsx( 'edit-site-global-styles__shadow-editor__remove-button', { 'is-open': isOpen } diff --git a/packages/edit-site/src/components/global-styles/shadows-panel.js b/packages/edit-site/src/components/global-styles/shadows-panel.js index 43e0c063f492b8..8e93ab2b15fb0a 100644 --- a/packages/edit-site/src/components/global-styles/shadows-panel.js +++ b/packages/edit-site/src/components/global-styles/shadows-panel.js @@ -8,10 +8,17 @@ import { Button, Flex, FlexItem, + privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, sprintf, isRTL } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; -import { plus, shadow as shadowIcon } from '@wordpress/icons'; +import { + plus, + Icon, + chevronLeft, + chevronRight, + moreVertical, +} from '@wordpress/icons'; /** * Internal dependencies @@ -21,8 +28,11 @@ import Subtitle from './subtitle'; import { NavigationButtonAsItem } from './navigation-button'; import ScreenHeader from './header'; import { getNewIndexFromPresets } from './utils'; +import { useState } from '@wordpress/element'; +import ConfirmResetShadowDialog from './confirm-reset-shadow-dialog'; const { useGlobalSetting } = unlock( blockEditorPrivateApis ); +const { Menu } = unlock( componentsPrivateApis ); export const defaultShadow = '6px 6px 9px rgba(0, 0, 0, 0.2)'; @@ -40,8 +50,27 @@ export default function ShadowsPanel() { setCustomShadows( [ ...( customShadows || [] ), shadow ] ); }; + const handleResetShadows = () => { + setCustomShadows( [] ); + }; + + const [ isResetDialogOpen, setIsResetDialogOpen ] = useState( false ); + + const toggleResetDialog = () => setIsResetDialogOpen( ! isResetDialogOpen ); + return ( <> + { isResetDialogOpen && ( + <ConfirmResetShadowDialog + text={ __( + 'Are you sure you want to remove all custom shadows?' + ) } + confirmButtonText={ __( 'Remove' ) } + isOpen={ isResetDialogOpen } + toggleOpen={ toggleResetDialog } + onConfirm={ handleResetShadows } + /> + ) } <ScreenHeader title={ __( 'Shadows' ) } description={ __( @@ -73,6 +102,7 @@ export default function ShadowsPanel() { category="custom" canCreate onCreate={ onCreateShadow } + onReset={ toggleResetDialog } /> </VStack> </div> @@ -80,7 +110,14 @@ export default function ShadowsPanel() { ); } -function ShadowList( { label, shadows, category, canCreate, onCreate } ) { +function ShadowList( { + label, + shadows, + category, + canCreate, + onCreate, + onReset, +} ) { const handleAddShadow = () => { const newIndex = getNewIndexFromPresets( shadows, 'shadow-' ); onCreate( { @@ -115,6 +152,26 @@ function ShadowList( { label, shadows, category, canCreate, onCreate } ) { /> </FlexItem> ) } + { !! shadows?.length && category === 'custom' && ( + <Menu> + <Menu.TriggerButton + render={ + <Button + size="small" + icon={ moreVertical } + label={ __( 'Shadow options' ) } + /> + } + /> + <Menu.Popover> + <Menu.Item onClick={ onReset }> + <Menu.ItemLabel> + { __( 'Remove all custom shadows' ) } + </Menu.ItemLabel> + </Menu.Item> + </Menu.Popover> + </Menu> + ) } </HStack> { shadows.length > 0 && ( <ItemGroup isBordered isSeparated> @@ -135,9 +192,11 @@ function ShadowItem( { shadow, category } ) { return ( <NavigationButtonAsItem path={ `/shadows/edit/${ category }/${ shadow.slug }` } - icon={ shadowIcon } > - { shadow.name } + <HStack> + <FlexItem>{ shadow.name }</FlexItem> + <Icon icon={ isRTL() ? chevronLeft : chevronRight } /> + </HStack> </NavigationButtonAsItem> ); } diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 68cc40c4b62066..99b1c8c92bbd02 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -169,10 +169,18 @@ top: $grid-unit; opacity: 0; + &.edit-site-global-styles__shadow-editor__remove-button { + border: none; + } + .edit-site-global-styles__shadow-editor__dropdown-toggle:hover + &, &:focus, &:hover { - border: none; + opacity: 1; + } + + @media (hover: none) { + // Show reset button on devices that do not support hover. opacity: 1; } } diff --git a/packages/edit-site/src/components/global-styles/variations/style.scss b/packages/edit-site/src/components/global-styles/variations/style.scss index 5f57c72f180b12..b092e09e487508 100644 --- a/packages/edit-site/src/components/global-styles/variations/style.scss +++ b/packages/edit-site/src/components/global-styles/variations/style.scss @@ -9,9 +9,10 @@ outline-offset: -$border-width; overflow: hidden; position: relative; - // Add the same transition that block style variations and other buttons have. - transition: outline 0.1s linear; - @include reduce-motion("transition"); + @media not (prefers-reduced-motion) { + // Add the same transition that block style variations and other buttons have. + transition: outline 0.1s linear; + } &.is-pill { height: $button-size-compact; diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 20162c5272f2ef..a5e14f0be82816 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -32,7 +32,8 @@ import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands import { privateApis as routerPrivateApis } from '@wordpress/router'; import { PluginArea } from '@wordpress/plugins'; import { store as noticesStore } from '@wordpress/notices'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -70,6 +71,15 @@ function Layout() { triggerAnimationOnChange: routeKey + '-' + canvas, } ); + const { showIconLabels } = useSelect( ( select ) => { + return { + showIconLabels: select( preferencesStore ).get( + 'core', + 'showIconLabels' + ), + }; + } ); + const [ backgroundColor ] = useGlobalStyle( 'color.background' ); const [ gradientValue ] = useGlobalStyle( 'color.gradient' ); const previousCanvaMode = usePrevious( canvas ); @@ -93,6 +103,7 @@ function Layout() { navigateRegionsProps.className, { 'is-full-canvas': canvas === 'edit', + 'show-icon-labels': showIconLabels, } ) } > diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 2c7e6ce1b10c8b..caf7dd78da4b34 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -115,10 +115,13 @@ .edit-site-resizable-frame__inner-content { box-shadow: $elevation-x-small; - transition: border-radius, box-shadow 0.4s; // This ensure the radius work properly. overflow: hidden; + @media not (prefers-reduced-motion) { + transition: border-radius, box-shadow 0.4s; + } + .edit-site-layout:not(.is-full-canvas) & { border-radius: $radius-large; } @@ -195,8 +198,6 @@ html.canvas-mode-edit-transition::view-transition-group(toggle) { } &::before { - transition: box-shadow 0.1s ease; - @include reduce-motion("transition"); content: ""; display: block; position: absolute; @@ -206,6 +207,10 @@ html.canvas-mode-edit-transition::view-transition-group(toggle) { left: 9px; border-radius: $radius-medium; box-shadow: none; + + @media not (prefers-reduced-motion) { + transition: box-shadow 0.1s ease; + } } .edit-site-layout__view-mode-toggle-icon { diff --git a/packages/edit-site/src/components/page-patterns/fields.js b/packages/edit-site/src/components/page-patterns/fields.js index f202664389f0ff..d884508e575068 100644 --- a/packages/edit-site/src/components/page-patterns/fields.js +++ b/packages/edit-site/src/components/page-patterns/fields.js @@ -15,50 +15,25 @@ import { } from '@wordpress/block-editor'; import { Icon } from '@wordpress/icons'; import { parse } from '@wordpress/blocks'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ import { - PATTERN_TYPES, TEMPLATE_PART_POST_TYPE, PATTERN_SYNC_TYPES, OPERATOR_IS, } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; import { useAddedBy } from '../page-templates/hooks'; -import { defaultGetTitle } from './search-items'; -const { useLink } = unlock( routerPrivateApis ); const { useGlobalStyle } = unlock( blockEditorPrivateApis ); -function PreviewWrapper( { item, onClick, ariaDescribedBy, children } ) { - return ( - <button - className="page-patterns-preview-field__button" - type="button" - onClick={ item.type !== PATTERN_TYPES.theme ? onClick : undefined } - aria-label={ defaultGetTitle( item ) } - aria-describedby={ ariaDescribedBy } - aria-disabled={ item.type === PATTERN_TYPES.theme } - > - { children } - </button> - ); -} - function PreviewField( { item } ) { const descriptionId = useId(); const description = item.description || item?.excerpt?.raw; - const isUserPattern = item.type === PATTERN_TYPES.user; const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; const [ backgroundColor ] = useGlobalStyle( 'color.background' ); - const { onClick } = useLink( - `/${ item.type }/${ - isUserPattern || isTemplatePart ? item.id : item.name - }?canvas=edit` - ); const blocks = useMemo( () => { return ( item.blocks ?? @@ -73,23 +48,18 @@ function PreviewField( { item } ) { <div className="page-patterns-preview-field" style={ { backgroundColor } } + aria-describedby={ !! description ? descriptionId : undefined } > - <PreviewWrapper - item={ item } - onClick={ onClick } - ariaDescribedBy={ !! description ? descriptionId : undefined } - > - { isEmpty && isTemplatePart && __( 'Empty template part' ) } - { isEmpty && ! isTemplatePart && __( 'Empty pattern' ) } - { ! isEmpty && ( - <BlockPreview.Async> - <BlockPreview - blocks={ blocks } - viewportWidth={ item.viewportWidth } - /> - </BlockPreview.Async> - ) } - </PreviewWrapper> + { isEmpty && isTemplatePart && __( 'Empty template part' ) } + { isEmpty && ! isTemplatePart && __( 'Empty pattern' ) } + { ! isEmpty && ( + <BlockPreview.Async> + <BlockPreview + blocks={ blocks } + viewportWidth={ item.viewportWidth } + /> + </BlockPreview.Async> + ) } { !! description && ( <div hidden id={ descriptionId }> { description } diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index 72d53c2a721afc..2cacc8fab607c2 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -12,28 +12,6 @@ width: 96px; flex-grow: 0; } - - .page-patterns-preview-field__button { - box-shadow: none; - border: none; - padding: 0; - background-color: unset; - box-sizing: border-box; - cursor: pointer; - overflow: hidden; - height: 100%; - border-radius: $grid-unit-05; - - &:focus-visible { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 2px solid transparent; - } - - &[aria-disabled="true"] { - cursor: default; - } - } } .edit-site-patterns__pattern-icon { @@ -48,10 +26,12 @@ top: 0; z-index: 2; flex-shrink: 0; - transition: padding ease-out 0.1s; - @include reduce-motion("transition"); min-height: $grid-unit-50; + @media not (prefers-reduced-motion) { + transition: padding ease-out 0.1s; + } + .edit-site-patterns__title { min-height: $grid-unit-50; @@ -114,7 +94,6 @@ } } -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @container (max-width: 430px) { .edit-site-page-patterns-dataviews .edit-site-patterns__section-header { padding-left: $grid-unit-30; diff --git a/packages/edit-site/src/components/page-templates/fields.js b/packages/edit-site/src/components/page-templates/fields.js index 88c20ff27ebbce..97b427690901a6 100644 --- a/packages/edit-site/src/components/page-templates/fields.js +++ b/packages/edit-site/src/components/page-templates/fields.js @@ -16,7 +16,6 @@ import { privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { EditorProvider } from '@wordpress/editor'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies @@ -25,7 +24,6 @@ import { useAddedBy } from './hooks'; import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; -const { useLink } = unlock( routerPrivateApis ); const { useGlobalStyle } = unlock( blockEditorPrivateApis ); function PreviewField( { item } ) { @@ -34,7 +32,6 @@ function PreviewField( { item } ) { const blocks = useMemo( () => { return parse( item.content.raw ); }, [ item.content.raw ] ); - const { onClick } = useLink( `/${ item.type }/${ item.id }?canvas=edit` ); const isEmpty = ! blocks?.length; // Wrap everything in a block editor provider to ensure 'styles' that are needed @@ -50,19 +47,12 @@ function PreviewField( { item } ) { className="page-templates-preview-field" style={ { backgroundColor } } > - <button - className="page-templates-preview-field__button" - type="button" - onClick={ onClick } - aria-label={ item.title?.rendered || item.title } - > - { isEmpty && __( 'Empty template' ) } - { ! isEmpty && ( - <BlockPreview.Async> - <BlockPreview blocks={ blocks } /> - </BlockPreview.Async> - ) } - </button> + { isEmpty && __( 'Empty template' ) } + { ! isEmpty && ( + <BlockPreview.Async> + <BlockPreview blocks={ blocks } /> + </BlockPreview.Async> + ) } </div> </EditorProvider> ); diff --git a/packages/edit-site/src/components/page-templates/style.scss b/packages/edit-site/src/components/page-templates/style.scss index 4432cf6bec4923..bb9069e2c5038a 100644 --- a/packages/edit-site/src/components/page-templates/style.scss +++ b/packages/edit-site/src/components/page-templates/style.scss @@ -5,24 +5,6 @@ width: 100%; border-radius: $radius-medium; - .page-templates-preview-field__button { - box-shadow: none; - border: none; - padding: 0; - background-color: unset; - box-sizing: border-box; - cursor: pointer; - overflow: hidden; - height: 100%; - border-radius: $radius-medium; - - &:focus-visible { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 2px solid transparent; - } - } - .dataviews-view-list & { .block-editor-block-preview__container { height: 120px; @@ -85,9 +67,11 @@ height: $grid-unit-20; object-fit: cover; opacity: 0; - transition: opacity 0.1s linear; - @include reduce-motion("transition"); border-radius: 100%; + + @media not (prefers-reduced-motion) { + transition: opacity 0.1s linear; + } } &.is-loaded { diff --git a/packages/edit-site/src/components/page/style.scss b/packages/edit-site/src/components/page/style.scss index 03e062a576b6e6..23e79420a7fbb2 100644 --- a/packages/edit-site/src/components/page/style.scss +++ b/packages/edit-site/src/components/page/style.scss @@ -4,8 +4,10 @@ height: calc(100% - #{$header-height}); /* stylelint-disable-next-line property-no-unknown -- '@container' not globally permitted */ container: edit-site-page / inline-size; - transition: width ease-out 0.2s; - @include reduce-motion("transition"); + + @media not (prefers-reduced-motion) { + transition: width ease-out 0.2s; + } @include break-medium() { height: 100%; @@ -19,8 +21,10 @@ position: sticky; top: 0; z-index: z-index(".edit-site-page-header"); - transition: padding ease-out 0.1s; - @include reduce-motion("transition"); + + @media not (prefers-reduced-motion) { + transition: padding ease-out 0.1s; + } .components-heading { color: $gray-900; @@ -41,7 +45,6 @@ } } -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ @container (max-width: 430px) { .edit-site-page-header { padding: $grid-unit-20 $grid-unit-30; diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index a67a505795b3c8..6ab3a47efb4653 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -109,6 +109,7 @@ function useView( postType ) { return { ...initialView, type, + ...defaultLayouts[ type ], }; } ); @@ -140,13 +141,15 @@ function useView( postType ) { // without affecting any other config. const onUrlLayoutChange = useEvent( () => { setView( ( prevView ) => { - const layoutToApply = layout ?? LAYOUT_LIST; - if ( layoutToApply === prevView.type ) { + const newType = layout ?? LAYOUT_LIST; + if ( newType === prevView.type ) { return prevView; } + return { ...prevView, - type: layout ?? LAYOUT_LIST, + type: newType, + ...defaultLayouts[ newType ], }; } ); } ); @@ -168,6 +171,7 @@ function useView( postType ) { setView( { ...newView, type, + ...defaultLayouts[ type ], } ); } } ); @@ -190,6 +194,10 @@ function getItemId( item ) { return item.id.toString(); } +function getItemLevel( item ) { + return item.level; +} + export default function PostList( { postType } ) { const [ view, setView ] = useView( postType ); const defaultViews = useDefaultViews( { postType } ); @@ -215,7 +223,6 @@ export default function PostList( { postType } ) { }, [ location.path, location.query.isCustom, history ] ); - const getActiveViewFilters = ( views, match ) => { const found = views.find( ( { slug } ) => slug === match ); return found?.filters ?? []; @@ -296,6 +303,7 @@ export default function PostList( { postType } ) { _embed: 'author', order: view.sort?.direction, orderby: view.sort?.field, + orderby_hierarchy: !! view.showLevels, search: view.search, ...filters, }; @@ -417,6 +425,7 @@ export default function PostList( { postType } ) { history.navigate( `/${ postType }/${ id }?canvas=edit` ); } } getItemId={ getItemId } + getItemLevel={ getItemLevel } defaultLayouts={ defaultLayouts } header={ window.__experimentalQuickEditDataViews && diff --git a/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js b/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js index 847029e8d6dcfe..463ce0003fba26 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js +++ b/packages/edit-site/src/components/sidebar-dataviews/custom-dataviews-list.js @@ -27,7 +27,7 @@ import DataViewItem from './dataview-item'; import AddNewItem from './add-new-view'; import { unlock } from '../../lock-unlock'; -const { useHistory } = unlock( routerPrivateApis ); +const { useHistory, useLocation } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; @@ -85,6 +85,7 @@ function RenameItemModalContent( { dataviewId, currentTitle, setIsRenaming } ) { function CustomDataViewItem( { dataviewId, isActive } ) { const history = useHistory(); + const location = useLocation(); const { dataview } = useSelect( ( select ) => { const { getEditedEntityRecord } = select( coreStore ); @@ -145,10 +146,10 @@ function CustomDataViewItem( { dataviewId, isActive } ) { } ); if ( isActive ) { - const { - params: { postType }, - } = history.getLocationWithParams(); - history.replace( { postType } ); + history.replace( { + postType: + location.query.postType, + } ); } onClose(); } } @@ -212,7 +213,7 @@ export default function CustomDataViewsList( { type, activeView, isCustom } ) { <div className="edit-site-sidebar-navigation-screen-dataviews__group-header"> <Heading level={ 2 }>{ __( 'Custom Views' ) }</Heading> </div> - <ItemGroup> + <ItemGroup className="edit-site-sidebar-navigation-screen-dataviews__custom-items"> { customDataViews.map( ( customViewRecord ) => { return ( <CustomDataViewItem 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 c08a2c1a57c58e..c6edf7d2dd1203 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -39,9 +39,10 @@ const DEFAULT_POST_BASE = { page: 1, perPage: 20, sort: { - field: 'date', - direction: 'desc', + field: 'title', + direction: 'asc', }, + showLevels: true, titleField: 'title', mediaField: 'featured_media', fields: [ 'author', 'status' ], diff --git a/packages/edit-site/src/components/sidebar-dataviews/style.scss b/packages/edit-site/src/components/sidebar-dataviews/style.scss index 6d4c01e1a222b6..36eabd0b4c079b 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/style.scss +++ b/packages/edit-site/src/components/sidebar-dataviews/style.scss @@ -12,9 +12,12 @@ margin-right: -$grid-unit-20; } +.edit-site-sidebar-navigation-screen-dataviews__custom-items .edit-site-sidebar-dataviews-dataview-item { + padding-right: $grid-unit-10; +} + .edit-site-sidebar-dataviews-dataview-item { border-radius: $radius-small; - padding-right: $grid-unit-10; .edit-site-sidebar-dataviews-dataview-item__dropdown-menu { min-width: initial; diff --git a/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js b/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js index 980f20c49821b0..de12bbe466bf3b 100644 --- a/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js +++ b/packages/edit-site/src/components/sidebar-global-styles-wrapper/index.js @@ -5,11 +5,9 @@ import { __ } from '@wordpress/i18n'; import { useMemo, useState } from '@wordpress/element'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useViewportMatch } from '@wordpress/compose'; -import { - Button, - privateApis as componentsPrivateApis, -} from '@wordpress/components'; -import { addQueryArgs } from '@wordpress/url'; +import { Button } from '@wordpress/components'; +import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { seen } from '@wordpress/icons'; /** * Internal dependencies @@ -17,58 +15,42 @@ import { addQueryArgs } from '@wordpress/url'; import GlobalStylesUI from '../global-styles/ui'; import Page from '../page'; import { unlock } from '../../lock-unlock'; -import StyleBook from '../style-book'; -import { STYLE_BOOK_COLOR_GROUPS } from '../style-book/constants'; const { useLocation, useHistory } = unlock( routerPrivateApis ); -const { Menu } = unlock( componentsPrivateApis ); const GlobalStylesPageActions = ( { isStyleBookOpened, setIsStyleBookOpened, + path, } ) => { + const history = useHistory(); return ( - <Menu - trigger={ - <Button __next40pxDefaultSize variant="tertiary" size="compact"> - { __( 'Preview' ) } - </Button> - } - > - <Menu.RadioItem - value - checked={ isStyleBookOpened } - name="styles-preview-actions" - onChange={ () => setIsStyleBookOpened( true ) } - defaultChecked - > - <Menu.ItemLabel>{ __( 'Style book' ) }</Menu.ItemLabel> - <Menu.ItemHelpText> - { __( 'Preview blocks and styles.' ) } - </Menu.ItemHelpText> - </Menu.RadioItem> - <Menu.RadioItem - value={ false } - checked={ ! isStyleBookOpened } - name="styles-preview-actions" - onChange={ () => setIsStyleBookOpened( false ) } - > - <Menu.ItemLabel>{ __( 'Site' ) }</Menu.ItemLabel> - <Menu.ItemHelpText> - { __( 'Preview your site.' ) } - </Menu.ItemHelpText> - </Menu.RadioItem> - </Menu> + <Button + isPressed={ isStyleBookOpened } + icon={ seen } + label={ __( 'Style Book' ) } + onClick={ () => { + setIsStyleBookOpened( ! isStyleBookOpened ); + const updatedPath = ! isStyleBookOpened + ? addQueryArgs( path, { preview: 'stylebook' } ) + : removeQueryArgs( path, 'preview' ); + // Navigate to the updated path. + history.navigate( updatedPath ); + } } + size="compact" + /> ); }; -export default function GlobalStylesUIWrapper() { +/** + * Hook to deal with navigation and location state. + * + * @return {Array} The current section and a function to update it. + */ +export const useSection = () => { const { path, query } = useLocation(); const history = useHistory(); - const { canvas = 'view' } = query; - const [ isStyleBookOpened, setIsStyleBookOpened ] = useState( false ); - const isMobileViewport = useViewportMatch( 'medium', '<' ); - const [ section, onChangeSection ] = useMemo( () => { + return useMemo( () => { return [ query.section ?? '/', ( updatedSection ) => { @@ -80,6 +62,16 @@ export default function GlobalStylesUIWrapper() { }, ]; }, [ path, query.section, history ] ); +}; + +export default function GlobalStylesUIWrapper() { + const { path } = useLocation(); + + const [ isStyleBookOpened, setIsStyleBookOpened ] = useState( + path.includes( 'preview=stylebook' ) + ); + const isMobileViewport = useViewportMatch( 'medium', '<' ); + const [ section, onChangeSection ] = useSection(); return ( <> @@ -89,6 +81,7 @@ export default function GlobalStylesUIWrapper() { <GlobalStylesPageActions isStyleBookOpened={ isStyleBookOpened } setIsStyleBookOpened={ setIsStyleBookOpened } + path={ path } /> ) : null } @@ -100,45 +93,6 @@ export default function GlobalStylesUIWrapper() { onPathChange={ onChangeSection } /> </Page> - { canvas === 'view' && isStyleBookOpened && ( - <StyleBook - enableResizing={ false } - showCloseButton={ false } - showTabs={ false } - isSelected={ ( blockName ) => - // Match '/blocks/core%2Fbutton' and - // '/blocks/core%2Fbutton/typography', but not - // '/blocks/core%2Fbuttons'. - section === - `/blocks/${ encodeURIComponent( blockName ) }` || - section.startsWith( - `/blocks/${ encodeURIComponent( blockName ) }/` - ) - } - path={ section } - onSelect={ ( blockName ) => { - if ( - STYLE_BOOK_COLOR_GROUPS.find( - ( group ) => group.slug === blockName - ) - ) { - // Go to color palettes Global Styles. - onChangeSection( '/colors/palette' ); - return; - } - if ( blockName === 'typography' ) { - // Go to typography Global Styles. - onChangeSection( '/typography' ); - return; - } - - // Now go to the selected block. - onChangeSection( - `/blocks/${ encodeURIComponent( blockName ) }` - ); - } } - /> - ) } </> ); } diff --git a/packages/edit-site/src/components/sidebar-global-styles-wrapper/style.scss b/packages/edit-site/src/components/sidebar-global-styles-wrapper/style.scss index 88aa9ddf0c1618..0fa4e158fe7f10 100644 --- a/packages/edit-site/src/components/sidebar-global-styles-wrapper/style.scss +++ b/packages/edit-site/src/components/sidebar-global-styles-wrapper/style.scss @@ -33,3 +33,25 @@ color: $gray-900; } } + +.show-icon-labels { + .edit-site-styles .edit-site-page-content { + .edit-site-page-header__actions { + .components-button.has-icon { + width: auto; + padding: 0 $grid-unit-10; + + // Hide the button icons when labels are set to display... + svg { + display: none; + } + // ... and display labels. + &::after { + content: attr(aria-label); + font-size: $helptext-font-size; + } + } + + } + } +} diff --git a/packages/edit-site/src/components/sidebar-navigation-item/style.scss b/packages/edit-site/src/components/sidebar-navigation-item/style.scss index ac1cf8b730861d..57b7e84bd57a8b 100644 --- a/packages/edit-site/src/components/sidebar-navigation-item/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-item/style.scss @@ -18,6 +18,12 @@ &[aria-current="true"] { background: $gray-800; color: $white; + font-weight: $font-weight-medium; + } + + // Make sure the focus style is drawn on top of the current item background. + &:focus-visible { + transform: translateZ(0); } .edit-site-sidebar-navigation-item__drilldown-indicator { diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js index e205de25636975..abcc7183f6604e 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-main/index.js @@ -19,7 +19,7 @@ import { store as editSiteStore } from '../../store'; export function MainSidebarNavigationContent( { isBlockBasedTheme = true } ) { return ( - <ItemGroup> + <ItemGroup className="edit-site-sidebar-navigation-screen-main"> { isBlockBasedTheme && ( <> <SidebarNavigationItem diff --git a/packages/edit-site/src/components/site-editor-routes/stylebook.js b/packages/edit-site/src/components/site-editor-routes/stylebook.js index a30c4a7c04945e..cb1e414098ab3f 100644 --- a/packages/edit-site/src/components/site-editor-routes/stylebook.js +++ b/packages/edit-site/src/components/site-editor-routes/stylebook.js @@ -22,7 +22,7 @@ export const stylebookRoute = { ) } /> ), - preview: <StyleBookPreview />, - mobile: <StyleBookPreview />, + preview: <StyleBookPreview isStatic />, + mobile: <StyleBookPreview isStatic />, }, }; diff --git a/packages/edit-site/src/components/site-editor-routes/styles.js b/packages/edit-site/src/components/site-editor-routes/styles.js index cf29dbebea3733..a1827bee763390 100644 --- a/packages/edit-site/src/components/site-editor-routes/styles.js +++ b/packages/edit-site/src/components/site-editor-routes/styles.js @@ -10,6 +10,7 @@ import Editor from '../editor'; import { unlock } from '../../lock-unlock'; import SidebarNavigationScreenGlobalStyles from '../sidebar-navigation-screen-global-styles'; import GlobalStylesUIWrapper from '../sidebar-global-styles-wrapper'; +import { StyleBookPreview } from '../style-book'; const { useLocation } = unlock( routerPrivateApis ); @@ -30,7 +31,10 @@ export const stylesRoute = { areas: { content: <GlobalStylesUIWrapper />, sidebar: <SidebarNavigationScreenGlobalStyles backPath="/" />, - preview: <Editor />, + preview( { query } ) { + const isStylebook = query.preview === 'stylebook'; + return isStylebook ? <StyleBookPreview /> : <Editor />; + }, mobile: <MobileGlobalStylesUI />, }, widths: { diff --git a/packages/edit-site/src/components/site-hub/style.scss b/packages/edit-site/src/components/site-hub/style.scss index 39f44d2bef7bb5..4099f7064ff05f 100644 --- a/packages/edit-site/src/components/site-hub/style.scss +++ b/packages/edit-site/src/components/site-hub/style.scss @@ -65,8 +65,10 @@ opacity: 0; position: absolute; right: 0; - transition: opacity 0.1s linear; - @include reduce-motion("transition"); + + @media not (prefers-reduced-motion) { + transition: opacity 0.1s linear; + } } &:hover::after, diff --git a/packages/edit-site/src/components/style-book/constants.ts b/packages/edit-site/src/components/style-book/constants.ts index 401d532b98cbb7..dcd41287fa239d 100644 --- a/packages/edit-site/src/components/style-book/constants.ts +++ b/packages/edit-site/src/components/style-book/constants.ts @@ -148,6 +148,55 @@ export const STYLE_BOOK_CATEGORIES: StyleBookCategory[] = [ }, ]; +// Style book preview subcategories for all blocks section. +export const STYLE_BOOK_ALL_BLOCKS_SUBCATEGORIES: StyleBookCategory[] = [ + ...STYLE_BOOK_THEME_SUBCATEGORIES, + { + slug: 'media', + title: __( 'Media' ), + blocks: [ 'core/post-featured-image' ], + }, + { + slug: 'widgets', + title: __( 'Widgets' ), + blocks: [], + }, + { + slug: 'embed', + title: __( 'Embeds' ), + include: [], + }, +]; + +// Style book preview categories are organised slightly differently to the editor ones. +export const STYLE_BOOK_PREVIEW_CATEGORIES: StyleBookCategory[] = [ + { + slug: 'overview', + title: __( 'Overview' ), + blocks: [], + }, + { + slug: 'text', + title: __( 'Text' ), + blocks: [ + 'core/post-content', + 'core/home-link', + 'core/navigation-link', + ], + }, + { + slug: 'colors', + title: __( 'Colors' ), + blocks: [], + }, + { + slug: 'blocks', + title: __( 'All Blocks' ), + blocks: [], + subcategories: STYLE_BOOK_ALL_BLOCKS_SUBCATEGORIES, + }, +]; + // Forming a "block formatting context" to prevent margin collapsing. // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context const ROOT_CONTAINER = ` @@ -239,7 +288,6 @@ export const STYLE_BOOK_IFRAME_STYLES = ` .edit-site-style-book__subcategory-title { font-size: 16px; margin-bottom: 40px; - border-bottom: 1px solid #ddd; padding-bottom: 8px; } diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index da69ed734166ed..723953777e2b28 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -32,8 +32,11 @@ import { useContext, useRef, useLayoutEffect, + useEffect, } from '@wordpress/element'; import { ENTER, SPACE } from '@wordpress/keycodes'; +import { uploadMedia } from '@wordpress/media-utils'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -47,6 +50,12 @@ import { } from './categories'; import { getExamples } from './examples'; import { store as siteEditorStore } from '../../store'; +import { useSection } from '../sidebar-global-styles-wrapper'; +import { GlobalStylesRenderer } from '../global-styles-renderer'; +import { + STYLE_BOOK_COLOR_GROUPS, + STYLE_BOOK_PREVIEW_CATEGORIES, +} from '../style-book/constants'; const { ExperimentalBlockEditorProvider, @@ -85,35 +94,24 @@ const scrollToSection = ( anchorId, iframe ) => { }; /** - * Parses a Block Editor navigation path to extract the block name and - * build a style book navigation path. The object can be extended to include a category, - * representing a style book tab/section. + * Parses a Block Editor navigation path to build a style book navigation path. + * The object can be extended to include a category, representing a style book tab/section. * * @param {string} path An internal Block Editor navigation path. * @return {null|{block: string}} An object containing the example to navigate to. */ const getStyleBookNavigationFromPath = ( path ) => { if ( path && typeof path === 'string' ) { - if ( path === '/' ) { + if ( + path === '/' || + path.startsWith( '/typography' ) || + path.startsWith( '/colors' ) || + path.startsWith( '/blocks' ) + ) { return { top: true, }; } - - if ( path.startsWith( '/typography' ) ) { - return { - block: 'typography', - }; - } - let block = path.includes( '/blocks/' ) - ? decodeURIComponent( path.split( '/blocks/' )[ 1 ] ) - : null; - // Default to theme-colors if the path ends with /colors. - block = path.endsWith( '/colors' ) ? 'theme-colors' : block; - - return { - block, - }; } return null; }; @@ -307,29 +305,43 @@ function StyleBook( { ) ) } </Tabs.TabList> </div> - { tabs.map( ( tab ) => ( - <Tabs.TabPanel - key={ tab.slug } - tabId={ tab.slug } - focusable={ false } - className="edit-site-style-book__tabpanel" - > - <StyleBookBody - category={ tab.slug } - examples={ examples } - isSelected={ isSelected } - onSelect={ onSelect } - settings={ settings } - sizes={ sizes } - title={ tab.title } - goTo={ goTo } - /> - </Tabs.TabPanel> - ) ) } + { tabs.map( ( tab ) => { + const categoryDefinition = tab.slug + ? getTopLevelStyleBookCategories().find( + ( _category ) => + _category.slug === tab.slug + ) + : null; + const filteredExamples = categoryDefinition + ? getExamplesByCategory( + categoryDefinition, + examples + ) + : { examples }; + return ( + <Tabs.TabPanel + key={ tab.slug } + tabId={ tab.slug } + focusable={ false } + className="edit-site-style-book__tabpanel" + > + <StyleBookBody + category={ tab.slug } + examples={ filteredExamples } + isSelected={ isSelected } + onSelect={ onSelect } + settings={ settings } + sizes={ sizes } + title={ tab.title } + goTo={ goTo } + /> + </Tabs.TabPanel> + ); + } ) } </Tabs> ) : ( <StyleBookBody - examples={ examplesForSinglePageUse } + examples={ { examples: examplesForSinglePageUse } } isSelected={ isSelected } onClick={ onClick } onSelect={ onSelect } @@ -346,33 +358,113 @@ function StyleBook( { /** * Style Book Preview component renders the stylebook without the Editor dependency. * - * @param {Object} props Component props. - * @param {string} props.path Path to the selected block. - * @param {Object} props.userConfig User configuration. - * @param {Function} props.isSelected Function to check if a block is selected. - * @param {Function} props.onSelect Function to select a block. + * @param {Object} props Component props. + * @param {Object} props.userConfig User configuration. + * @param {boolean} props.isStatic Whether the stylebook is static or clickable. * @return {Object} Style Book Preview component. */ -export const StyleBookPreview = ( { - path = '', - userConfig = {}, - isSelected, - onSelect, -} ) => { +export const StyleBookPreview = ( { userConfig = {}, isStatic = false } ) => { const siteEditorSettings = useSelect( ( select ) => select( siteEditorStore ).getSettings(), [] ); + + const canUserUploadMedia = useSelect( + ( select ) => + select( coreStore ).canUser( 'create', { + kind: 'root', + name: 'media', + } ), + [] + ); + // Update block editor settings because useMultipleOriginColorsAndGradients fetch colours from there. - dispatch( blockEditorStore ).updateSettings( siteEditorSettings ); + useEffect( () => { + dispatch( blockEditorStore ).updateSettings( { + ...siteEditorSettings, + mediaUpload: canUserUploadMedia ? uploadMedia : undefined, + } ); + }, [ siteEditorSettings, canUserUploadMedia ] ); + + const [ section, onChangeSection ] = useSection(); + + const isSelected = ( blockName ) => { + // Match '/blocks/core%2Fbutton' and + // '/blocks/core%2Fbutton/typography', but not + // '/blocks/core%2Fbuttons'. + return ( + section === `/blocks/${ encodeURIComponent( blockName ) }` || + section.startsWith( + `/blocks/${ encodeURIComponent( blockName ) }/` + ) + ); + }; + + const onSelect = ( blockName ) => { + if ( + STYLE_BOOK_COLOR_GROUPS.find( + ( group ) => group.slug === blockName + ) + ) { + // Go to color palettes Global Styles. + onChangeSection( '/colors/palette' ); + return; + } + if ( blockName === 'typography' ) { + // Go to typography Global Styles. + onChangeSection( '/typography' ); + return; + } + + // Now go to the selected block. + onChangeSection( `/blocks/${ encodeURIComponent( blockName ) }` ); + }; const [ resizeObserver, sizes ] = useResizeObserver(); const colors = useMultiOriginPalettes(); const examples = getExamples( colors ); const examplesForSinglePageUse = getExamplesForSinglePageUse( examples ); + let previewCategory = null; + if ( section.includes( '/colors' ) ) { + previewCategory = 'colors'; + } else if ( section.includes( '/typography' ) ) { + previewCategory = 'text'; + } else if ( section.includes( '/blocks' ) ) { + previewCategory = 'blocks'; + const blockName = + decodeURIComponent( section ).split( '/blocks/' )[ 1 ]; + if ( + blockName && + examples.find( ( example ) => example.name === blockName ) + ) { + previewCategory = blockName; + } + } else if ( ! isStatic ) { + previewCategory = 'overview'; + } + const categoryDefinition = STYLE_BOOK_PREVIEW_CATEGORIES.find( + ( category ) => category.slug === previewCategory + ); + + // If there's no category definition there may be a single block. + const filteredExamples = categoryDefinition + ? getExamplesByCategory( categoryDefinition, examples ) + : { + examples: [ + examples.find( + ( example ) => example.name === previewCategory + ), + ], + }; + + // If there's no preview category, show all examples. + const displayedExamples = previewCategory + ? filteredExamples + : { examples: examplesForSinglePageUse }; + const { base: baseConfig } = useContext( GlobalStylesContext ); - const goTo = getStyleBookNavigationFromPath( path ); + const goTo = getStyleBookNavigationFromPath( section ); const mergedConfig = useMemo( () => { if ( ! isObjectEmpty( userConfig ) && ! isObjectEmpty( baseConfig ) ) { @@ -399,13 +491,14 @@ export const StyleBookPreview = ( { <div className="edit-site-style-book"> { resizeObserver } <BlockEditorProvider settings={ settings }> + <GlobalStylesRenderer disableRootPadding /> <StyleBookBody - examples={ examplesForSinglePageUse } + examples={ displayedExamples } settings={ settings } goTo={ goTo } sizes={ sizes } - isSelected={ isSelected } - onSelect={ onSelect } + isSelected={ ! isStatic ? isSelected : null } + onSelect={ ! isStatic ? onSelect : null } /> </BlockEditorProvider> </div> @@ -413,7 +506,6 @@ export const StyleBookPreview = ( { }; export const StyleBookBody = ( { - category, examples, isSelected, onClick, @@ -459,13 +551,6 @@ export const StyleBookBody = ( { if ( hasIframeLoaded && iframeRef?.current ) { if ( goTo?.top ) { scrollToSection( 'top', iframeRef?.current ); - return; - } - if ( goTo?.block ) { - scrollToSection( - `example-${ goTo?.block }`, - iframeRef?.current - ); } } }, [ iframeRef?.current, goTo, scrollToSection, hasIframeLoaded ] ); @@ -492,8 +577,7 @@ export const StyleBookBody = ( { className={ clsx( 'edit-site-style-book__examples', { 'is-wide': sizes.width > 600, } ) } - examples={ examples } - category={ category } + filteredExamples={ examples } label={ title ? sprintf( @@ -505,24 +589,14 @@ export const StyleBookBody = ( { } isSelected={ isSelected } onSelect={ onSelect } - key={ category } + key={ title } /> </Iframe> ); }; const Examples = memo( - ( { className, examples, category, label, isSelected, onSelect } ) => { - const categoryDefinition = category - ? getTopLevelStyleBookCategories().find( - ( _category ) => _category.slug === category - ) - : null; - - const filteredExamples = categoryDefinition - ? getExamplesByCategory( categoryDefinition, examples ) - : { examples }; - + ( { className, filteredExamples, label, isSelected, onSelect } ) => { return ( <Composite orientation="vertical" diff --git a/packages/edit-widgets/src/components/layout/style.scss b/packages/edit-widgets/src/components/layout/style.scss index 14d74e4db9248c..71b1049adf196d 100644 --- a/packages/edit-widgets/src/components/layout/style.scss +++ b/packages/edit-widgets/src/components/layout/style.scss @@ -7,13 +7,6 @@ } } -.edit-widgets-layout__inserter-panel-header { - padding-top: $grid-unit-10; - padding-right: $grid-unit-10; - display: flex; - justify-content: flex-end; -} - .edit-widgets-layout__inserter-panel-content { // Leave space for the close button height: calc(100% - #{$button-size} - #{$grid-unit-10}); diff --git a/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js b/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js index 4b26dd306ea0a3..72e04e5f62034c 100644 --- a/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js +++ b/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js @@ -1,8 +1,6 @@ /** * WordPress dependencies */ -import { Button, VisuallyHidden } from '@wordpress/components'; -import { close } from '@wordpress/icons'; import { __experimentalLibrary as Library } from '@wordpress/block-editor'; import { useViewportMatch, @@ -10,7 +8,6 @@ import { } from '@wordpress/compose'; import { useCallback, useRef } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -28,7 +25,6 @@ export default function InserterSidebar() { return setIsInserterOpened( false ); }, [ setIsInserterOpened ] ); - const TagName = ! isMobileViewport ? VisuallyHidden : 'div'; const [ inserterDialogRef, inserterDialogProps ] = useDialog( { onClose: closeInserter, focusOnMount: true, @@ -42,14 +38,6 @@ export default function InserterSidebar() { { ...inserterDialogProps } className="edit-widgets-layout__inserter-panel" > - <TagName className="edit-widgets-layout__inserter-panel-header"> - <Button - __next40pxDefaultSize - icon={ close } - onClick={ closeInserter } - label={ __( 'Close Block Inserter' ) } - /> - </TagName> <div className="edit-widgets-layout__inserter-panel-content"> <Library showInserterHelpPanel @@ -57,6 +45,7 @@ export default function InserterSidebar() { rootClientId={ rootClientId } __experimentalInsertionIndex={ insertionIndex } ref={ libraryRef } + onClose={ closeInserter } /> </div> </div> diff --git a/packages/edit-widgets/src/store/transformers.js b/packages/edit-widgets/src/store/transformers.js index 3b42e3141ff5f0..12a2f9d32933a8 100644 --- a/packages/edit-widgets/src/store/transformers.js +++ b/packages/edit-widgets/src/store/transformers.js @@ -46,7 +46,7 @@ export function transformWidgetToBlock( widget ) { * Converts a block to a widget entity record. * * @param {Object} block The block. - * @param {Object?} relatedWidget A related widget entity record from the API (optional). + * @param {?Object} relatedWidget A related widget entity record from the API (optional). * @return {Object} the widget object (converted from block). */ export function transformBlockToWidget( block, relatedWidget = {} ) { diff --git a/packages/editor/README.md b/packages/editor/README.md index 3211e6664256d0..c006ec097982c9 100644 --- a/packages/editor/README.md +++ b/packages/editor/README.md @@ -499,6 +499,7 @@ _Parameters_ - _$0.maxUploadFileSize_ `?number`: Maximum upload size in bytes allowed for the site. - _$0.onError_ `Function`: Function called when an error happens. - _$0.onFileChange_ `Function`: Function called each time a file or a temporary representation of the file is available. +- _$0.onSuccess_ `Function`: Function called after the final representation of the file is available. ### MediaUploadCheck diff --git a/packages/editor/src/components/commands/index.js b/packages/editor/src/components/commands/index.js index 0040a09fbdc07d..d495dcaaef3379 100644 --- a/packages/editor/src/components/commands/index.js +++ b/packages/editor/src/components/commands/index.js @@ -25,6 +25,7 @@ import { store as noticesStore } from '@wordpress/notices'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore, useEntityRecord } from '@wordpress/core-data'; import { store as interfaceStore } from '@wordpress/interface'; +import { getPath } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; /** @@ -90,6 +91,19 @@ const getEditorCommandLoader = () => const { openModal, enableComplementaryArea, disableComplementaryArea } = useDispatch( interfaceStore ); const { getCurrentPostId } = useSelect( editorStore ); + const { isBlockBasedTheme, canCreateTemplate } = useSelect( + ( select ) => { + return { + isBlockBasedTheme: + select( coreStore ).getCurrentTheme()?.is_block_theme, + canCreateTemplate: select( coreStore ).canUser( 'create', { + kind: 'postType', + name: 'wp_template', + } ), + }; + }, + [] + ); const allowSwitchEditorMode = isCodeEditingEnabled && isRichEditingEnabled; @@ -271,6 +285,21 @@ const getEditorCommandLoader = () => }, } ); } + if ( canCreateTemplate && isBlockBasedTheme ) { + const isSiteEditor = getPath( window.location.href )?.includes( + 'site-editor.php' + ); + if ( ! isSiteEditor ) { + commands.push( { + name: 'core/go-to-site-editor', + label: __( 'Open Site Editor' ), + callback: ( { close } ) => { + close(); + document.location = 'site-editor.php'; + }, + } ); + } + } return { commands, diff --git a/packages/editor/src/components/document-bar/index.js b/packages/editor/src/components/document-bar/index.js index f5ca65dfe18ed7..544b5024d88a89 100644 --- a/packages/editor/src/components/document-bar/index.js +++ b/packages/editor/src/components/document-bar/index.js @@ -22,6 +22,7 @@ import { store as commandsStore } from '@wordpress/commands'; import { useRef, useEffect } from '@wordpress/element'; import { useReducedMotion } from '@wordpress/compose'; import { decodeEntities } from '@wordpress/html-entities'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies @@ -200,7 +201,7 @@ export default function DocumentBar( props ) { <Text size="body" as="h1"> <span className="editor-document-bar__post-title"> { title - ? decodeEntities( title ) + ? stripHTML( title ) : __( 'No title' ) } </span> { pageTypeBadge && ( diff --git a/packages/editor/src/components/document-tools/index.js b/packages/editor/src/components/document-tools/index.js index a98def685e93a6..71a8b1b094a135 100644 --- a/packages/editor/src/components/document-tools/index.js +++ b/packages/editor/src/components/document-tools/index.js @@ -10,7 +10,7 @@ import { useViewportMatch } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; import { __, _x } from '@wordpress/i18n'; import { NavigableToolbar, ToolSelector } from '@wordpress/block-editor'; -import { Button, ToolbarItem } from '@wordpress/components'; +import { ToolbarButton, ToolbarItem } from '@wordpress/components'; import { listView, plus } from '@wordpress/icons'; import { useCallback } from '@wordpress/element'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; @@ -118,9 +118,8 @@ function DocumentTools( { className, disableBlockTools = false } ) { > <div className="editor-document-tools__left"> { ! isDistractionFree && ( - <ToolbarItem + <ToolbarButton ref={ inserterSidebarToggleRef } - as={ Button } className="editor-document-tools__inserter-toggle" variant="primary" isPressed={ isInserterOpened } @@ -159,8 +158,7 @@ function DocumentTools( { className, disableBlockTools = false } ) { size="compact" /> { ! isDistractionFree && ( - <ToolbarItem - as={ Button } + <ToolbarButton className="editor-document-tools__document-overview-toggle" icon={ listView } disabled={ disableBlockTools } @@ -175,7 +173,6 @@ function DocumentTools( { className, disableBlockTools = false } ) { } aria-expanded={ isListViewOpen } ref={ listViewToggleRef } - size="compact" /> ) } </> diff --git a/packages/editor/src/components/document-tools/style.scss b/packages/editor/src/components/document-tools/style.scss index a1abfd5abd7aef..dfafff2126d66d 100644 --- a/packages/editor/src/components/document-tools/style.scss +++ b/packages/editor/src/components/document-tools/style.scss @@ -74,14 +74,8 @@ } .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; - padding: 0; - .show-icon-labels & { width: auto; - height: $button-size-compact; padding: 0 $grid-unit-10; } } diff --git a/packages/editor/src/components/editor-help/intro-to-blocks.native.js b/packages/editor/src/components/editor-help/intro-to-blocks.native.js index 3dc23ec2619172..9e23a70936d4e9 100644 --- a/packages/editor/src/components/editor-help/intro-to-blocks.native.js +++ b/packages/editor/src/components/editor-help/intro-to-blocks.native.js @@ -71,7 +71,7 @@ const IntroToBlocks = () => { <HelpDetailSectionHeadingText text={ __( 'Build layouts' ) } /> <HelpDetailBodyText text={ __( - 'Arrange your content into columns, add Call to Action buttons, and overlay images with text.' + 'Arrange your content into columns, add Call to action buttons, and overlay images with text.' ) } /> <HelpDetailImage diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index ad584b0df75574..200473cccff706 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -115,6 +115,10 @@ export function EntitiesSavedStatesExtensible( { 'description' ); + const selectItemsToSaveDescription = !! dirtyEntityRecords.length + ? __( 'Select the items you want to save.' ) + : undefined; + return ( <div ref={ renderDialog ? saveDialogRef : undefined } @@ -180,7 +184,7 @@ export function EntitiesSavedStatesExtensible( { ), { strong: <strong /> } ) - : __( 'Select the items you want to save.' ) } + : selectItemsToSaveDescription } </p> </div> diff --git a/packages/editor/src/components/error-boundary/index.native.js b/packages/editor/src/components/error-boundary/index.native.js index 0de048e8114451..4c05ceb3fc150b 100644 --- a/packages/editor/src/components/error-boundary/index.native.js +++ b/packages/editor/src/components/error-boundary/index.native.js @@ -16,7 +16,7 @@ import { usePreferredColorSchemeStyle, withPreferredColorScheme, } from '@wordpress/compose'; -import { warning } from '@wordpress/icons'; +import { cautionFilled } from '@wordpress/icons'; import { Icon } from '@wordpress/components'; /** @@ -141,7 +141,7 @@ class ErrorBoundary extends Component { <View style={ styles[ 'error-boundary__container' ] }> <View style={ iconContainerStyle }> <Icon - icon={ warning } + icon={ cautionFilled } { ...styles[ 'error-boundary__icon' ] } /> </View> diff --git a/packages/editor/src/components/more-menu/index.js b/packages/editor/src/components/more-menu/index.js index 9e062e5e5adc50..f5eaa45e4ed696 100644 --- a/packages/editor/src/components/more-menu/index.js +++ b/packages/editor/src/components/more-menu/index.js @@ -113,7 +113,6 @@ export default function MoreMenu() { <ActionItem.Slot name="core/plugin-more-menu" label={ __( 'Plugins' ) } - as={ MenuGroup } fillProps={ { onClick: onClose } } /> <MenuGroup label={ __( 'Tools' ) }> diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 808134ea969a11..023b93d31bb511 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -11,6 +11,7 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import { useSetAsHomepageAction } from './set-as-homepage'; +import { useSetAsPostsPageAction } from './set-as-posts-page'; export function usePostActions( { postType, onActionPerformed, context } ) { const { defaultActions } = useSelect( @@ -43,7 +44,8 @@ export function usePostActions( { postType, onActionPerformed, context } ) { ); const setAsHomepageAction = useSetAsHomepageAction(); - const shouldShowSetAsHomepageAction = + const setAsPostsPageAction = useSetAsPostsPageAction(); + const shouldShowHomepageActions = canManageOptions && ! hasFrontPageTemplate; const { registerPostTypeSchema } = unlock( useDispatch( editorStore ) ); @@ -53,10 +55,15 @@ export function usePostActions( { postType, onActionPerformed, context } ) { return useMemo( () => { let actions = [ ...defaultActions ]; - if ( shouldShowSetAsHomepageAction ) { - actions.push( setAsHomepageAction ); + if ( shouldShowHomepageActions ) { + actions.push( setAsHomepageAction, setAsPostsPageAction ); } + // Ensure "Move to trash" is always the last action. + actions = actions.sort( ( a, b ) => + b.id === 'move-to-trash' ? -1 : 0 + ); + // Filter actions based on provided context. If not provided // all actions are returned. We'll have a single entry for getting the actions // and the consumer should provide the context to filter the actions, if needed. @@ -123,6 +130,7 @@ export function usePostActions( { postType, onActionPerformed, context } ) { defaultActions, onActionPerformed, setAsHomepageAction, - shouldShowSetAsHomepageAction, + setAsPostsPageAction, + shouldShowHomepageActions, ] ); } diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index 4b541d52d429cc..d6adf6c0721667 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -74,24 +74,26 @@ export default function PostActions( { postType, postId, onActionPerformed } ) { return ( <> - <Menu - trigger={ - <Button - size="small" - icon={ moreVertical } - label={ __( 'Actions' ) } - disabled={ ! actions.length } - accessibleWhenDisabled - className="editor-all-actions-button" - /> - } - placement="bottom-end" - > - <ActionsDropdownMenuGroup - actions={ actions } - items={ itemsWithPermissions } - setActiveModalAction={ setActiveModalAction } + <Menu placement="bottom-end"> + <Menu.TriggerButton + render={ + <Button + size="small" + icon={ moreVertical } + label={ __( 'Actions' ) } + disabled={ ! actions.length } + accessibleWhenDisabled + className="editor-all-actions-button" + /> + } /> + <Menu.Popover> + <ActionsDropdownMenuGroup + actions={ actions } + items={ itemsWithPermissions } + setActiveModalAction={ setActiveModalAction } + /> + </Menu.Popover> </Menu> { !! activeModalAction && ( <ActionModal diff --git a/packages/editor/src/components/post-actions/set-as-homepage.js b/packages/editor/src/components/post-actions/set-as-homepage.js index 70bdeeeefe70fb..cb67e251ed58c2 100644 --- a/packages/editor/src/components/post-actions/set-as-homepage.js +++ b/packages/editor/src/components/post-actions/set-as-homepage.js @@ -12,20 +12,11 @@ import { import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { store as noticesStore } from '@wordpress/notices'; -import { decodeEntities } from '@wordpress/html-entities'; -const getItemTitle = ( item ) => { - if ( typeof item.title === 'string' ) { - return decodeEntities( item.title ); - } - if ( item.title && 'rendered' in item.title ) { - return decodeEntities( item.title.rendered ); - } - if ( item.title && 'raw' in item.title ) { - return decodeEntities( item.title.raw ); - } - return ''; -}; +/** + * Internal dependencies + */ +import { getItemTitle } from '../../utils/get-item-title'; const SetAsHomepageModal = ( { items, closeModal } ) => { const [ item ] = items; @@ -47,12 +38,8 @@ const SetAsHomepageModal = ( { items, closeModal } ) => { }; } ); - const currentHomePageTitle = currentHomePage - ? getItemTitle( currentHomePage ) - : ''; - const { saveEditedEntityRecord, saveEntityRecord } = - useDispatch( coreStore ); + const { saveEntityRecord } = useDispatch( coreStore ); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); @@ -60,52 +47,44 @@ const SetAsHomepageModal = ( { items, closeModal } ) => { event.preventDefault(); try { - // Save new home page settings. - await saveEditedEntityRecord( 'root', 'site', undefined, { - page_on_front: item.id, - show_on_front: 'page', - } ); - - // This second call to a save function is a workaround for a bug in - // `saveEditedEntityRecord`. This forces the root site settings to be updated. - // See https://github.com/WordPress/gutenberg/issues/67161. await saveEntityRecord( 'root', 'site', { page_on_front: item.id, show_on_front: 'page', } ); - createSuccessNotice( __( 'Homepage updated' ), { + createSuccessNotice( __( 'Homepage updated.' ), { type: 'snackbar', } ); } catch ( error ) { - const typedError = error; const errorMessage = - typedError.message && typedError.code !== 'unknown_error' - ? typedError.message - : __( 'An error occurred while setting the homepage' ); + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while setting the homepage.' ); createErrorNotice( errorMessage, { type: 'snackbar' } ); } finally { closeModal?.(); } } - const modalWarning = - 'posts' === showOnFront - ? __( - 'This will replace the current homepage which is set to display latest posts.' - ) - : sprintf( - // translators: %s: title of the current home page. - __( 'This will replace the current homepage: "%s"' ), - currentHomePageTitle - ); + let modalWarning = ''; + if ( 'posts' === showOnFront ) { + modalWarning = __( + 'This will replace the current homepage which is set to display latest posts.' + ); + } else if ( currentHomePage ) { + modalWarning = sprintf( + // translators: %s: title of the current home page. + __( 'This will replace the current homepage: "%s"' ), + getItemTitle( currentHomePage ) + ); + } const modalText = sprintf( // translators: %1$s: title of the page to be set as the homepage, %2$s: homepage replacement warning message. __( 'Set "%1$s" as the site homepage? %2$s' ), pageTitle, modalWarning - ); + ).trim(); // translators: Button label to confirm setting the specified page as the homepage. const modalButtonLabel = __( 'Set homepage' ); @@ -143,8 +122,13 @@ const SetAsHomepageModal = ( { items, closeModal } ) => { export const useSetAsHomepageAction = () => { const { pageOnFront, pageForPosts } = useSelect( ( select ) => { - const { getEntityRecord } = select( coreStore ); - const siteSettings = getEntityRecord( 'root', 'site' ); + const { getEntityRecord, canUser } = select( coreStore ); + const siteSettings = canUser( 'read', { + kind: 'root', + name: 'site', + } ) + ? getEntityRecord( 'root', 'site' ) + : undefined; return { pageOnFront: siteSettings?.page_on_front, pageForPosts: siteSettings?.page_for_posts, diff --git a/packages/editor/src/components/post-actions/set-as-posts-page.js b/packages/editor/src/components/post-actions/set-as-posts-page.js new file mode 100644 index 00000000000000..830c2cac734f1f --- /dev/null +++ b/packages/editor/src/components/post-actions/set-as-posts-page.js @@ -0,0 +1,164 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useMemo } from '@wordpress/element'; +import { + Button, + __experimentalText as Text, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { getItemTitle } from '../../utils/get-item-title'; + +const SetAsPostsPageModal = ( { items, closeModal } ) => { + const [ item ] = items; + const pageTitle = getItemTitle( item ); + const { currentPostsPage, isPageForPostsSet, isSaving } = useSelect( + ( select ) => { + const { getEntityRecord, isSavingEntityRecord } = + select( coreStore ); + const siteSettings = getEntityRecord( 'root', 'site' ); + const currentPostsPageItem = getEntityRecord( + 'postType', + 'page', + siteSettings?.page_for_posts + ); + return { + currentPostsPage: currentPostsPageItem, + isPageForPostsSet: siteSettings?.page_for_posts !== 0, + isSaving: isSavingEntityRecord( 'root', 'site' ), + }; + } + ); + + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function onSetPageAsPostsPage( event ) { + event.preventDefault(); + + try { + await saveEntityRecord( 'root', 'site', { + page_for_posts: item.id, + show_on_front: 'page', + } ); + + createSuccessNotice( __( 'Posts page updated.' ), { + type: 'snackbar', + } ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( 'An error occurred while setting the posts page.' ); + createErrorNotice( errorMessage, { type: 'snackbar' } ); + } finally { + closeModal?.(); + } + } + + const modalWarning = + isPageForPostsSet && currentPostsPage + ? sprintf( + // translators: %s: title of the current posts page. + __( 'This will replace the current posts page: "%s"' ), + getItemTitle( currentPostsPage ) + ) + : __( 'This page will show the latest posts.' ); + + const modalText = sprintf( + // translators: %1$s: title of the page to be set as the posts page, %2$s: posts page replacement warning message. + __( 'Set "%1$s" as the posts page? %2$s' ), + pageTitle, + modalWarning + ); + + // translators: Button label to confirm setting the specified page as the posts page. + const modalButtonLabel = __( 'Set posts page' ); + + return ( + <form onSubmit={ onSetPageAsPostsPage }> + <VStack spacing="5"> + <Text>{ modalText }</Text> + <HStack justify="right"> + <Button + __next40pxDefaultSize + variant="tertiary" + onClick={ () => { + closeModal?.(); + } } + disabled={ isSaving } + accessibleWhenDisabled + > + { __( 'Cancel' ) } + </Button> + <Button + __next40pxDefaultSize + variant="primary" + type="submit" + disabled={ isSaving } + accessibleWhenDisabled + > + { modalButtonLabel } + </Button> + </HStack> + </VStack> + </form> + ); +}; + +export const useSetAsPostsPageAction = () => { + const { pageOnFront, pageForPosts } = useSelect( ( select ) => { + const { getEntityRecord, canUser } = select( coreStore ); + const siteSettings = canUser( 'read', { + kind: 'root', + name: 'site', + } ) + ? getEntityRecord( 'root', 'site' ) + : undefined; + + return { + pageOnFront: siteSettings?.page_on_front, + pageForPosts: siteSettings?.page_for_posts, + }; + } ); + + return useMemo( + () => ( { + id: 'set-as-posts-page', + label: __( 'Set as posts page' ), + isEligible( post ) { + if ( post.status !== 'publish' ) { + return false; + } + + if ( post.type !== 'page' ) { + return false; + } + + // Don't show the action if the page is already set as the homepage. + if ( pageOnFront === post.id ) { + return false; + } + + // Don't show the action if the page is already set as the page for posts. + if ( pageForPosts === post.id ) { + return false; + } + + return true; + }, + RenderModal: SetAsPostsPageModal, + } ), + [ pageForPosts, pageOnFront ] + ); +}; diff --git a/packages/editor/src/components/post-card-panel/index.js b/packages/editor/src/components/post-card-panel/index.js index 7849f014ab49c8..895545cb007f00 100644 --- a/packages/editor/src/components/post-card-panel/index.js +++ b/packages/editor/src/components/post-card-panel/index.js @@ -6,12 +6,13 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, __experimentalText as Text, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import { decodeEntities } from '@wordpress/html-entities'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies @@ -25,6 +26,7 @@ import { unlock } from '../../lock-unlock'; import PostActions from '../post-actions'; import usePageTypeBadge from '../../utils/pageTypeBadge'; import { getTemplateInfo } from '../../utils/get-template-info'; +const { Badge } = unlock( componentsPrivateApis ); /** * Renders a title of the post type and the available quick actions available within a 3-dot dropdown. @@ -92,7 +94,7 @@ export default function PostCardPanel( { labels?.name ); } else if ( postTitle ) { - title = decodeEntities( postTitle ); + title = stripHTML( postTitle ); } return ( @@ -109,11 +111,11 @@ export default function PostCardPanel( { className="editor-post-card-panel__title" as="h2" > - { title } + <span className="editor-post-card-panel__title-name"> + { title } + </span> { pageTypeBadge && postIds.length === 1 && ( - <span className="editor-post-card-panel__title-badge"> - { pageTypeBadge } - </span> + <Badge>{ pageTypeBadge }</Badge> ) } </Text> <PostActions diff --git a/packages/editor/src/components/post-card-panel/style.scss b/packages/editor/src/components/post-card-panel/style.scss index c3638b313a8285..5fa54c67f14e55 100644 --- a/packages/editor/src/components/post-card-panel/style.scss +++ b/packages/editor/src/components/post-card-panel/style.scss @@ -9,7 +9,6 @@ &.editor-post-card-panel__title { @include heading-medium(); margin: 0; - padding: 2px 0; display: flex; align-items: center; flex-wrap: wrap; @@ -34,19 +33,11 @@ margin-bottom: $grid-unit-10; } + .editor-post-card-panel__title-name { + padding: 2px 0; + } + .editor-post-card-panel__description { color: $gray-700; } } - -.editor-post-card-panel__title-badge { - background: $gray-100; - color: $gray-800; - padding: 0 $grid-unit-05; - border-radius: $radius-small; - font-size: 12px; - font-weight: 400; - flex-shrink: 0; - line-height: $grid-unit-05 * 5; - display: inline-block; -} diff --git a/packages/editor/src/components/post-publish-panel/maybe-upload-media.js b/packages/editor/src/components/post-publish-panel/maybe-upload-media.js index 32ea69c425e0b5..a92a4794154344 100644 --- a/packages/editor/src/components/post-publish-panel/maybe-upload-media.js +++ b/packages/editor/src/components/post-publish-panel/maybe-upload-media.js @@ -9,7 +9,7 @@ import { __unstableAnimatePresence as AnimatePresence, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useState } from '@wordpress/element'; import { isBlobURL } from '@wordpress/blob'; @@ -260,7 +260,7 @@ export default function MaybeUploadMediaPanel() { variant="primary" onClick={ uploadImages } > - { __( 'Upload' ) } + { _x( 'Upload', 'verb' ) } </Button> ) } </div> diff --git a/packages/editor/src/components/preferences-modal/index.js b/packages/editor/src/components/preferences-modal/index.js index 72042bca03b70c..fba60405e7e4b5 100644 --- a/packages/editor/src/components/preferences-modal/index.js +++ b/packages/editor/src/components/preferences-modal/index.js @@ -26,7 +26,6 @@ import PageAttributesCheck from '../page-attributes/check'; import PostTypeSupportCheck from '../post-type-support-check'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -import { useStartPatterns } from '../start-page-options'; const { PreferencesModal, @@ -73,7 +72,6 @@ function PreferencesModalContents( { extraSections = {} } ) { const { setIsListViewOpened, setIsInserterOpened } = useDispatch( editorStore ); const { set: setPreference } = useDispatch( preferencesStore ); - const hasStarterPatterns = !! useStartPatterns().length; const sections = useMemo( () => @@ -114,16 +112,14 @@ function PreferencesModalContents( { extraSections = {} } ) { 'Allow right-click contextual menus' ) } /> - { hasStarterPatterns && ( - <PreferenceToggleControl - scope="core" - featureName="enableChoosePatternModal" - help={ __( - 'Shows starter patterns when creating a new page.' - ) } - label={ __( 'Show starter patterns' ) } - /> - ) } + <PreferenceToggleControl + scope="core" + featureName="enableChoosePatternModal" + help={ __( + 'Shows starter patterns when creating a new page.' + ) } + label={ __( 'Show starter patterns' ) } + /> </PreferencesModalSection> <PreferencesModalSection title={ __( 'Document settings' ) } @@ -341,7 +337,6 @@ function PreferencesModalContents( { extraSections = {} } ) { setIsListViewOpened, setPreference, isLargeViewport, - hasStarterPatterns, ] ); diff --git a/packages/editor/src/components/preview-dropdown/index.js b/packages/editor/src/components/preview-dropdown/index.js index 6fa35c673430cc..a081564e48ea8d 100644 --- a/packages/editor/src/components/preview-dropdown/index.js +++ b/packages/editor/src/components/preview-dropdown/index.js @@ -190,7 +190,6 @@ export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { ) } <ActionItem.Slot name="core/plugin-preview-menu" - as={ MenuGroup } fillProps={ { onClick: onClose } } /> </> diff --git a/packages/editor/src/components/provider/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/disable-non-page-content-blocks.js index ae4fd1075fc261..ffbf1ac0625463 100644 --- a/packages/editor/src/components/provider/disable-non-page-content-blocks.js +++ b/packages/editor/src/components/provider/disable-non-page-content-blocks.js @@ -16,9 +16,13 @@ import usePostContentBlocks from './use-post-content-blocks'; */ export default function DisableNonPageContentBlocks() { const contentOnlyIds = usePostContentBlocks(); - const templateParts = useSelect( ( select ) => { - const { getBlocksByName } = select( blockEditorStore ); - return getBlocksByName( 'core/template-part' ); + const { templateParts, isNavigationMode } = useSelect( ( select ) => { + const { getBlocksByName, isNavigationMode: _isNavigationMode } = + select( blockEditorStore ); + return { + templateParts: getBlocksByName( 'core/template-part' ), + isNavigationMode: _isNavigationMode(), + }; }, [] ); const disabledIds = useSelect( ( select ) => { @@ -32,38 +36,85 @@ export default function DisableNonPageContentBlocks() { const registry = useRegistry(); + // The code here is split into multiple `useEffects` calls. + // This is done to avoid setting/unsetting block editing modes multiple times unnecessarily. + // + // For example, the block editing mode of the root block (clientId: '') only + // needs to be set once, not when `contentOnlyIds` or `disabledIds` change. + // + // It's also unlikely that these different types of blocks are being inserted + // or removed at the same time, so using different effects reflects that. + useEffect( () => { + const { setBlockEditingMode, unsetBlockEditingMode } = + registry.dispatch( blockEditorStore ); + + setBlockEditingMode( '', 'disabled' ); + + return () => { + unsetBlockEditingMode( '' ); + }; + }, [ registry ] ); + useEffect( () => { const { setBlockEditingMode, unsetBlockEditingMode } = registry.dispatch( blockEditorStore ); registry.batch( () => { - setBlockEditingMode( '', 'disabled' ); for ( const clientId of contentOnlyIds ) { setBlockEditingMode( clientId, 'contentOnly' ); } - for ( const clientId of templateParts ) { - setBlockEditingMode( clientId, 'contentOnly' ); - } - for ( const clientId of disabledIds ) { - setBlockEditingMode( clientId, 'disabled' ); - } } ); return () => { registry.batch( () => { - unsetBlockEditingMode( '' ); for ( const clientId of contentOnlyIds ) { unsetBlockEditingMode( clientId ); } + } ); + }; + }, [ contentOnlyIds, registry ] ); + + useEffect( () => { + const { setBlockEditingMode, unsetBlockEditingMode } = + registry.dispatch( blockEditorStore ); + + registry.batch( () => { + if ( ! isNavigationMode ) { for ( const clientId of templateParts ) { - unsetBlockEditingMode( clientId ); + setBlockEditingMode( clientId, 'contentOnly' ); } + } + } ); + + return () => { + registry.batch( () => { + if ( ! isNavigationMode ) { + for ( const clientId of templateParts ) { + unsetBlockEditingMode( clientId ); + } + } + } ); + }; + }, [ templateParts, isNavigationMode, registry ] ); + + useEffect( () => { + const { setBlockEditingMode, unsetBlockEditingMode } = + registry.dispatch( blockEditorStore ); + + registry.batch( () => { + for ( const clientId of disabledIds ) { + setBlockEditingMode( clientId, 'disabled' ); + } + } ); + + return () => { + registry.batch( () => { for ( const clientId of disabledIds ) { unsetBlockEditingMode( clientId ); } } ); }; - }, [ templateParts, contentOnlyIds, disabledIds, registry ] ); + }, [ disabledIds, registry ] ); return null; } diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 68d7bd1d3f4f5b..1259eae623de93 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -13,7 +13,6 @@ import { BlockEditorProvider, BlockContextProvider, privateApis as blockEditorPrivateApis, - store as blockEditorStore, } from '@wordpress/block-editor'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; @@ -164,6 +163,7 @@ export const ExperimentalEditorProvider = withRegistryProvider( BlockEditorProviderComponent = ExperimentalBlockEditorProvider, __unstableTemplate: template, } ) => { + const hasTemplate = !! template; const { editorSettings, selection, @@ -196,7 +196,9 @@ export const ExperimentalEditorProvider = withRegistryProvider( isReady: __unstableIsEditorReady(), mode: getRenderingMode(), defaultMode: - postTypeObject?.default_rendering_mode ?? 'post-only', + hasTemplate && postTypeObject?.default_rendering_mode + ? postTypeObject?.default_rendering_mode + : 'post-only', selection: getEditorSelection(), postTypeEntities: post.type === 'wp_template' @@ -204,17 +206,9 @@ export const ExperimentalEditorProvider = withRegistryProvider( : null, }; }, - [ post.type ] + [ post.type, hasTemplate ] ); - const isZoomOut = useSelect( ( select ) => { - const { isZoomOut: _isZoomOut } = unlock( - select( blockEditorStore ) - ); - - return _isZoomOut(); - } ); - const shouldRenderTemplate = !! template && mode !== 'post-only'; const rootLevelPost = shouldRenderTemplate ? template : post; const defaultBlockContext = useMemo( () => { @@ -309,15 +303,11 @@ export const ExperimentalEditorProvider = withRegistryProvider( } ); } - }, [ - createWarningNotice, - initialEdits, - settings, - post, - recovery, - setupEditor, - updatePostLock, - ] ); + + // The dependencies of the hook are omitted deliberately + // We only want to run setupEditor (with initialEdits) only once per post. + // A better solution in the future would be to split this effect into multiple ones. + }, [] ); // Synchronizes the active post with the state useEffect( () => { @@ -367,13 +357,9 @@ export const ExperimentalEditorProvider = withRegistryProvider( { children } { ! settings.isPreviewMode && ( <> - { ! isZoomOut && ( - <> - <PatternsMenuItems /> - <TemplatePartMenuItems /> - <ContentOnlySettingsMenu /> - </> - ) } + <PatternsMenuItems /> + <TemplatePartMenuItems /> + <ContentOnlySettingsMenu /> { mode === 'template-locked' && ( <DisableNonPageContentBlocks /> ) } 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 f5c45f431e2c85..d0c2e36d474433 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -23,6 +23,7 @@ import { */ import inserterMediaCategories from '../media-categories'; import { mediaUpload } from '../../utils'; +import { default as mediaSideload } from '../../utils/media-sideload'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import { useGlobalStylesContext } from '../global-styles-provider'; @@ -45,6 +46,7 @@ const BLOCK_EDITOR_SETTINGS = [ '__experimentalGlobalStylesBaseStyles', 'alignWide', 'blockInspectorTabs', + 'maxUploadFileSize', 'allowedMimeTypes', 'bodyPlaceholder', 'canLockBlocks', @@ -290,6 +292,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { isDistractionFree, keepCaretInsideBlock, mediaUpload: hasUploadPermissions ? mediaUpload : undefined, + mediaSideload: hasUploadPermissions ? mediaSideload : undefined, __experimentalBlockPatterns: blockPatterns, [ selectBlockPatternsKey ]: ( select ) => { const { hasFinishedResolution, getBlockPatternsForPostType } = diff --git a/packages/editor/src/components/start-page-options/index.js b/packages/editor/src/components/start-page-options/index.js index 783a4a224fa378..d7874000ffc420 100644 --- a/packages/editor/src/components/start-page-options/index.js +++ b/packages/editor/src/components/start-page-options/index.js @@ -1,16 +1,8 @@ /** * WordPress dependencies */ -import { Modal } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { useState, useMemo } from '@wordpress/element'; -import { - store as blockEditorStore, - __experimentalBlockPatternsList as BlockPatternsList, -} from '@wordpress/block-editor'; +import { useEffect } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { __unstableSerializeAndClean } from '@wordpress/blocks'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as interfaceStore } from '@wordpress/interface'; @@ -18,124 +10,41 @@ import { store as interfaceStore } from '@wordpress/interface'; * Internal dependencies */ import { store as editorStore } from '../../store'; -import { TEMPLATE_POST_TYPE } from '../../store/constants'; - -export function useStartPatterns() { - // A pattern is a start pattern if it includes 'core/post-content' in its blockTypes, - // and it has no postTypes declared and the current post type is page or if - // the current post type is part of the postTypes declared. - const { blockPatternsWithPostContentBlockType, postType } = useSelect( - ( select ) => { - const { getPatternsByBlockTypes, getBlocksByName } = - select( blockEditorStore ); - const { getCurrentPostType, getRenderingMode } = - select( editorStore ); - const rootClientId = - getRenderingMode() === 'post-only' - ? '' - : getBlocksByName( 'core/post-content' )?.[ 0 ]; - return { - blockPatternsWithPostContentBlockType: getPatternsByBlockTypes( - 'core/post-content', - rootClientId - ), - postType: getCurrentPostType(), - }; - }, - [] - ); - - return useMemo( () => { - if ( ! blockPatternsWithPostContentBlockType?.length ) { - return []; - } - - /* - * Filter patterns without postTypes declared if the current postType is page - * or patterns that declare the current postType in its post type array. - */ - return blockPatternsWithPostContentBlockType.filter( ( pattern ) => { - return ( - ( postType === 'page' && ! pattern.postTypes ) || - ( Array.isArray( pattern.postTypes ) && - pattern.postTypes.includes( postType ) ) - ); - } ); - }, [ postType, blockPatternsWithPostContentBlockType ] ); -} - -function PatternSelection( { blockPatterns, onChoosePattern } ) { - const { editEntityRecord } = useDispatch( coreStore ); - const { postType, postId } = useSelect( ( select ) => { - const { getCurrentPostType, getCurrentPostId } = select( editorStore ); - - return { - postType: getCurrentPostType(), - postId: getCurrentPostId(), - }; - }, [] ); - return ( - <BlockPatternsList - blockPatterns={ blockPatterns } - onClickPattern={ ( _pattern, blocks ) => { - editEntityRecord( 'postType', postType, postId, { - blocks, - content: ( { blocks: blocksForSerialization = [] } ) => - __unstableSerializeAndClean( blocksForSerialization ), - } ); - onChoosePattern(); - } } - /> - ); -} - -function StartPageOptionsModal( { onClose } ) { - const startPatterns = useStartPatterns(); - const hasStartPattern = startPatterns.length > 0; - - if ( ! hasStartPattern ) { - return null; - } - - return ( - <Modal - title={ __( 'Choose a pattern' ) } - isFullScreen - onRequestClose={ onClose } - > - <div className="editor-start-page-options__modal-content"> - <PatternSelection - blockPatterns={ startPatterns } - onChoosePattern={ onClose } - /> - </div> - </Modal> - ); -} export default function StartPageOptions() { - const [ isClosed, setIsClosed ] = useState( false ); - const shouldEnableModal = useSelect( ( select ) => { - const { isEditedPostDirty, isEditedPostEmpty, getCurrentPostType } = - select( editorStore ); + const { postId, shouldEnable } = useSelect( ( select ) => { + const { + isEditedPostDirty, + isEditedPostEmpty, + getCurrentPostId, + getCurrentPostType, + } = select( editorStore ); const preferencesModalActive = select( interfaceStore ).isModalActive( 'editor/preferences' ); const choosePatternModalEnabled = select( preferencesStore ).get( 'core', 'enableChoosePatternModal' ); - return ( - choosePatternModalEnabled && - ! preferencesModalActive && - ! isEditedPostDirty() && - isEditedPostEmpty() && - TEMPLATE_POST_TYPE !== getCurrentPostType() - ); + return { + postId: getCurrentPostId(), + shouldEnable: + choosePatternModalEnabled && + ! preferencesModalActive && + ! isEditedPostDirty() && + isEditedPostEmpty() && + 'page' === getCurrentPostType(), + }; }, [] ); + const { setIsInserterOpened } = useDispatch( editorStore ); + + useEffect( () => { + if ( shouldEnable ) { + setIsInserterOpened( { + tab: 'patterns', + category: 'core/starter-content', + } ); + } + }, [ postId, shouldEnable, setIsInserterOpened ] ); - if ( ! shouldEnableModal || isClosed ) { - return null; - } - - return <StartPageOptionsModal onClose={ () => setIsClosed( true ) } />; + return null; } diff --git a/packages/editor/src/components/template-part-menu-items/index.js b/packages/editor/src/components/template-part-menu-items/index.js index 0e126644d49938..52c50f91b3933c 100644 --- a/packages/editor/src/components/template-part-menu-items/index.js +++ b/packages/editor/src/components/template-part-menu-items/index.js @@ -27,25 +27,16 @@ export default function TemplatePartMenuItems() { } function TemplatePartConverterMenuItem( { clientIds, onClose } ) { - const { isContentOnly, blocks } = useSelect( + const { blocks } = useSelect( ( select ) => { - const { getBlocksByClientId, getBlockEditingMode } = - select( blockEditorStore ); + const { getBlocksByClientId } = select( blockEditorStore ); return { blocks: getBlocksByClientId( clientIds ), - isContentOnly: - clientIds.length === 1 && - getBlockEditingMode( clientIds[ 0 ] ) === 'contentOnly', }; }, [ clientIds ] ); - // Do not show the convert button if the block is in content-only mode. - if ( isContentOnly ) { - return null; - } - // Allow converting a single template part to standard blocks. if ( blocks.length === 1 && blocks[ 0 ]?.name === 'core/template-part' ) { return ( diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 9d0de08718cd2b..6a628512f62bf7 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -23,7 +23,6 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { TRASH_POST_NOTICE_ID } from './constants'; import { localAutosaveSet } from './local-autosave'; import { getNotificationArgumentsForSaveSuccess, @@ -347,7 +346,6 @@ export const trashPost = const postType = await registry .resolveSelect( coreStore ) .getPostType( postTypeSlug ); - registry.dispatch( noticesStore ).removeNotice( TRASH_POST_NOTICE_ID ); const { rest_base: restBase, rest_namespace: restNamespace = 'wp/v2' } = postType; dispatch( { type: 'REQUEST_POST_DELETE_START' } ); diff --git a/packages/editor/src/store/constants.ts b/packages/editor/src/store/constants.ts index 73d6a104370c37..2cb0903453466e 100644 --- a/packages/editor/src/store/constants.ts +++ b/packages/editor/src/store/constants.ts @@ -11,8 +11,6 @@ export const EDIT_MERGE_PROPERTIES = new Set( [ 'meta' ] ); */ export const STORE_NAME = 'core/editor'; -export const SAVE_POST_NOTICE_ID = 'SAVE_POST_NOTICE_ID'; -export const TRASH_POST_NOTICE_ID = 'TRASH_POST_NOTICE_ID'; export const PERMALINK_POSTNAME_REGEX = /%(?:postname|pagename)%/; export const ONE_MINUTE_IN_MS = 60 * 1000; export const AUTOSAVE_PROPERTIES = [ 'title', 'excerpt', 'content' ]; diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js index 74c1f1ea100b37..ef2eb093f793e4 100644 --- a/packages/editor/src/store/private-actions.js +++ b/packages/editor/src/store/private-actions.js @@ -34,7 +34,7 @@ export function setCurrentTemplateId( id ) { /** * Create a block based template. * - * @param {Object?} template Template to create and assign. + * @param {?Object} template Template to create and assign. */ export const createTemplate = ( template ) => diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 58fc9ca0d747eb..9e1230b2ea88c5 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -3,11 +3,6 @@ */ import { __ } from '@wordpress/i18n'; -/** - * Internal dependencies - */ -import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../constants'; - /** * Builds the arguments for a success notification dispatch. * @@ -68,7 +63,7 @@ export function getNotificationArgumentsForSaveSuccess( data ) { return [ noticeMessage, { - id: SAVE_POST_NOTICE_ID, + id: 'editor-save', type: 'snackbar', actions, }, @@ -113,7 +108,7 @@ export function getNotificationArgumentsForSaveFail( data ) { return [ noticeMessage, { - id: SAVE_POST_NOTICE_ID, + id: 'editor-save', }, ]; } @@ -131,7 +126,7 @@ export function getNotificationArgumentsForTrashFail( data ) { ? data.error.message : __( 'Trashing failed' ), { - id: TRASH_POST_NOTICE_ID, + id: 'editor-trash-fail', }, ]; } diff --git a/packages/editor/src/store/utils/test/notice-builder.js b/packages/editor/src/store/utils/test/notice-builder.js index e66a96259680f7..d97ec0f9f9483b 100644 --- a/packages/editor/src/store/utils/test/notice-builder.js +++ b/packages/editor/src/store/utils/test/notice-builder.js @@ -6,7 +6,6 @@ import { getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from '../notice-builder'; -import { SAVE_POST_NOTICE_ID, TRASH_POST_NOTICE_ID } from '../../constants'; describe( 'getNotificationArgumentsForSaveSuccess()', () => { const postType = { @@ -27,7 +26,7 @@ describe( 'getNotificationArgumentsForSaveSuccess()', () => { }; const post = { ...previousPost }; const defaultExpectedAction = { - id: SAVE_POST_NOTICE_ID, + id: 'editor-save', actions: [], type: 'snackbar', }; @@ -106,7 +105,7 @@ describe( 'getNotificationArgumentsForSaveFail()', () => { const error = { code: '42', message: 'Something went wrong.' }; const post = { status: 'publish' }; const edits = { status: 'publish' }; - const defaultExpectedAction = { id: SAVE_POST_NOTICE_ID }; + const defaultExpectedAction = { id: 'editor-save' }; [ [ 'when error code is `rest_autosave_no_changes`', @@ -190,7 +189,7 @@ describe( 'getNotificationArgumentsForTrashFail()', () => { ].forEach( ( [ description, error, message ] ) => { // eslint-disable-next-line jest/valid-title it( description, () => { - const expectedValue = [ message, { id: TRASH_POST_NOTICE_ID } ]; + const expectedValue = [ message, { id: 'editor-trash-fail' } ]; expect( getNotificationArgumentsForTrashFail( { error } ) ).toEqual( expectedValue ); diff --git a/packages/editor/src/utils/get-item-title.js b/packages/editor/src/utils/get-item-title.js new file mode 100644 index 00000000000000..86929c27408a81 --- /dev/null +++ b/packages/editor/src/utils/get-item-title.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Helper function to get the title of a post item. + * This is duplicated from the `@wordpress/fields` package. + * `packages/fields/src/actions/utils.ts` + * + * @param {Object} item The post item. + * @return {string} The title of the item, or an empty string if the title is not found. + */ +export function getItemTitle( item ) { + if ( typeof item.title === 'string' ) { + return decodeEntities( item.title ); + } + if ( item.title && 'rendered' in item.title ) { + return decodeEntities( item.title.rendered ); + } + if ( item.title && 'raw' in item.title ) { + return decodeEntities( item.title.raw ); + } + return ''; +} diff --git a/packages/editor/src/utils/media-sideload/index.js b/packages/editor/src/utils/media-sideload/index.js new file mode 100644 index 00000000000000..86fcdc688abf8f --- /dev/null +++ b/packages/editor/src/utils/media-sideload/index.js @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +import { privateApis } from '@wordpress/media-utils'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { sideloadMedia: mediaSideload } = unlock( privateApis ); + +export default mediaSideload; diff --git a/packages/editor/src/utils/media-sideload/index.native.js b/packages/editor/src/utils/media-sideload/index.native.js new file mode 100644 index 00000000000000..d84a912ec92de0 --- /dev/null +++ b/packages/editor/src/utils/media-sideload/index.native.js @@ -0,0 +1 @@ +export default function mediaSideload() {} diff --git a/packages/editor/src/utils/media-upload/index.js b/packages/editor/src/utils/media-upload/index.js index 6b39db2443cc33..0d970d91ce745c 100644 --- a/packages/editor/src/utils/media-upload/index.js +++ b/packages/editor/src/utils/media-upload/index.js @@ -27,6 +27,7 @@ const noop = () => {}; * @param {?number} $0.maxUploadFileSize Maximum upload size in bytes allowed for the site. * @param {Function} $0.onError Function called when an error happens. * @param {Function} $0.onFileChange Function called each time a file or a temporary representation of the file is available. + * @param {Function} $0.onSuccess Function called after the final representation of the file is available. */ export default function mediaUpload( { additionalData = {}, @@ -35,6 +36,7 @@ export default function mediaUpload( { maxUploadFileSize, onError = noop, onFileChange, + onSuccess, } ) { const { getCurrentPost, getEditorSettings } = select( editorStore ); const { @@ -77,8 +79,9 @@ export default function mediaUpload( { } else { clearSaveLock(); } - onFileChange( file ); + onFileChange?.( file ); }, + onSuccess, additionalData: { ...postData, ...additionalData, diff --git a/packages/editor/tsconfig.json b/packages/editor/tsconfig.json index 3c45fbcb10db3d..00a8f3860e2925 100644 --- a/packages/editor/tsconfig.json +++ b/packages/editor/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "checkJs": false }, "references": [ @@ -34,6 +32,5 @@ { "path": "../url" }, { "path": "../warning" }, { "path": "../wordcount" } - ], - "include": [ "src" ] + ] } diff --git a/packages/element/tsconfig.json b/packages/element/tsconfig.json index ad6a489d33e9a5..a1df062eb218b3 100644 --- a/packages/element/tsconfig.json +++ b/packages/element/tsconfig.json @@ -2,12 +2,8 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", - "noImplicitAny": false, "strictNullChecks": false }, - "references": [ { "path": "../escape-html" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../escape-html" } ] } diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 0298e21c810009..7e87ba6371fdba 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,11 +2,19 @@ ## Unreleased +### Enhancements + +- Add support for WordPress multisite installations. Enabled via the new `multisite` environment config ([#67845](https://github.com/WordPress/gutenberg/pull/67845)). + +### Internal + +- Refactored the code to use new API introduced together with `@inquirer/prompts` instead of legacy `inquirer` package ([#67877](https://github.com/WordPress/gutenberg/pull/67877)). + ## 10.14.0 (2024-12-11) ### Enhancements -- Add phpMyAdmin as an optional service. Enabled via the new `phpmyadminPort` environment config, as well as env vars `WP_ENV_PHPMYADMIN_PORT` and `WP_ENV_TESTS_PHPMYADMIN_PORT`. +- Add phpMyAdmin as an optional service. Enabled via the new `phpmyadminPort` environment config, as well as env vars `WP_ENV_PHPMYADMIN_PORT` and `WP_ENV_TESTS_PHPMYADMIN_PORT` ([#67588](https://github.com/WordPress/gutenberg/pull/67588)). ### Internal diff --git a/packages/env/lib/commands/destroy.js b/packages/env/lib/commands/destroy.js index 46b923dc3c9aca..016838ea218442 100644 --- a/packages/env/lib/commands/destroy.js +++ b/packages/env/lib/commands/destroy.js @@ -5,7 +5,7 @@ const { v2: dockerCompose } = require( 'docker-compose' ); const fs = require( 'fs' ).promises; const path = require( 'path' ); -const inquirer = require( 'inquirer' ); +const { confirm } = require( '@inquirer/prompts' ); /** * Promisified dependencies @@ -40,14 +40,19 @@ module.exports = async function destroy( { spinner, scripts, debug } ) { 'WARNING! This will remove Docker containers, volumes, networks, and images associated with the WordPress instance.' ); - const { yesDelete } = await inquirer.prompt( [ - { - type: 'confirm', - name: 'yesDelete', + let yesDelete = false; + try { + yesDelete = await confirm( { message: 'Are you sure you want to continue?', default: false, - }, - ] ); + } ); + } catch ( error ) { + if ( error.name === 'ExitPromptError' ) { + console.log( 'Cancelled.' ); + process.exit( 1 ); + } + throw error; + } spinner.start(); diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index e476fd8c2b67b7..db05b82060d2c5 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -6,7 +6,7 @@ const { v2: dockerCompose } = require( 'docker-compose' ); const util = require( 'util' ); const path = require( 'path' ); const fs = require( 'fs' ).promises; -const inquirer = require( 'inquirer' ); +const { confirm } = require( '@inquirer/prompts' ); /** * Promisified dependencies @@ -328,15 +328,21 @@ async function checkForLegacyInstall( spinner ) { ' and ' ) }. Installs are now in your home folder.\n` ); - const { yesDelete } = await inquirer.prompt( [ - { - type: 'confirm', - name: 'yesDelete', + let yesDelete = false; + try { + yesDelete = confirm( { message: 'Do you wish to delete these old installs to reclaim disk space?', default: true, - }, - ] ); + } ); + } catch ( error ) { + if ( error.name === 'ExitPromptError' ) { + console.log( 'Cancelled.' ); + process.exit( 1 ); + } + throw error; + } + if ( yesDelete ) { await Promise.all( installs.map( ( install ) => rimraf( install ) ) ); spinner.info( 'Old installs deleted successfully.' ); diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index bddd7bc72aaee0..d5e06c5bb9beed 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -52,6 +52,7 @@ const mergeConfigs = require( './merge-configs' ); * @property {number} port The port to use. * @property {number} mysqlPort The port to use for MySQL. Random if empty. * @property {number} phpmyadminPort The port to use for phpMyAdmin. If empty, disabled phpMyAdmin. + * @property {boolean} multisite Whether to set up a multisite installation. * @property {Object} config Mapping of wp-config.php constants to their desired values. * @property {Object.<string, WPSource>} mappings Mapping of WordPress directories to local directories which should be mounted. * @property {string|null} phpVersion Version of PHP to use in the environments, of the format 0.0. @@ -89,6 +90,7 @@ const DEFAULT_ENVIRONMENT_CONFIG = { testsPort: 8889, mysqlPort: null, phpmyadminPort: null, + multisite: false, mappings: {}, config: { FS_METHOD: 'direct', @@ -466,6 +468,10 @@ async function parseEnvironmentConfig( parsedConfig.phpmyadminPort = config.phpmyadminPort; } + if ( config.multisite !== undefined ) { + parsedConfig.multisite = config.multisite; + } + if ( config.phpVersion !== undefined ) { // Support null as a valid input. if ( config.phpVersion !== null ) { diff --git a/packages/env/lib/config/test/__snapshots__/config-integration.js.snap b/packages/env/lib/config/test/__snapshots__/config-integration.js.snap index 6b671a6bc858eb..833a8a54d7749a 100644 --- a/packages/env/lib/config/test/__snapshots__/config-integration.js.snap +++ b/packages/env/lib/config/test/__snapshots__/config-integration.js.snap @@ -29,6 +29,7 @@ exports[`Config Integration should load local and override configuration files 1 "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "multisite": false, "mysqlPort": 23306, "phpVersion": null, "phpmyadminPort": null, @@ -59,6 +60,7 @@ exports[`Config Integration should load local and override configuration files 1 "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "multisite": false, "mysqlPort": 23307, "phpVersion": null, "phpmyadminPort": null, @@ -106,6 +108,7 @@ exports[`Config Integration should load local configuration file 1`] = ` "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "multisite": false, "mysqlPort": 13306, "phpVersion": null, "phpmyadminPort": null, @@ -136,6 +139,7 @@ exports[`Config Integration should load local configuration file 1`] = ` "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "multisite": false, "mysqlPort": 23307, "phpVersion": null, "phpmyadminPort": null, @@ -183,6 +187,7 @@ exports[`Config Integration should use default configuration 1`] = ` "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "multisite": false, "mysqlPort": null, "phpVersion": null, "phpmyadminPort": null, @@ -213,6 +218,7 @@ exports[`Config Integration should use default configuration 1`] = ` "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "multisite": false, "mysqlPort": null, "phpVersion": null, "phpmyadminPort": null, @@ -260,6 +266,7 @@ exports[`Config Integration should use environment variables over local and over "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "multisite": false, "mysqlPort": 23306, "phpVersion": null, "phpmyadminPort": null, @@ -291,6 +298,7 @@ exports[`Config Integration should use environment variables over local and over "url": "https://github.com/WordPress/WordPress.git", }, "mappings": {}, + "multisite": false, "mysqlPort": 23307, "phpVersion": null, "phpmyadminPort": null, diff --git a/packages/env/lib/config/test/parse-config.js b/packages/env/lib/config/test/parse-config.js index cc6e2c7a96bbc0..968c8b66a4ec15 100644 --- a/packages/env/lib/config/test/parse-config.js +++ b/packages/env/lib/config/test/parse-config.js @@ -23,6 +23,7 @@ const DEFAULT_CONFIG = { testsPort: 8889, mysqlPort: null, phpmyadminPort: null, + multisite: false, phpVersion: null, coreSource: { type: 'git', diff --git a/packages/env/lib/wordpress.js b/packages/env/lib/wordpress.js index bd3c4a23f8ff5d..8c08fb1f20ec78 100644 --- a/packages/env/lib/wordpress.js +++ b/packages/env/lib/wordpress.js @@ -86,11 +86,40 @@ async function configureWordPress( environment, config, spinner ) { // Ignore error. } - const installCommand = `wp core install --url="${ config.env[ environment ].config.WP_SITEURL }" --title="${ config.name }" --admin_user=admin --admin_password=password --admin_email=wordpress@example.com --skip-email`; + const isMultisite = config.env[ environment ].multisite; + + const installMethod = isMultisite ? 'multisite-install' : 'install'; + const installCommand = `wp core ${ installMethod } --url="${ config.env[ environment ].config.WP_SITEURL }" --title="${ config.name }" --admin_user=admin --admin_password=password --admin_email=wordpress@example.com --skip-email`; // -eo pipefail exits the command as soon as anything fails in bash. const setupCommands = [ 'set -eo pipefail', installCommand ]; + // Bootstrap .htaccess for multisite + if ( isMultisite ) { + // Using a subshell with `exec` was the best tradeoff I could come up + // with between readability of this source and compatibility with the + // way that all strings in `setupCommands` are later joined with '&&'. + setupCommands.push( + `( +exec > /var/www/html/.htaccess +echo 'RewriteEngine On' +echo 'RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]' +echo 'RewriteBase /' +echo 'RewriteRule ^index\.php$ - [L]' +echo '' +echo '# add a trailing slash to /wp-admin' +echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]' +echo '' +echo 'RewriteCond %{REQUEST_FILENAME} -f [OR]' +echo 'RewriteCond %{REQUEST_FILENAME} -d' +echo 'RewriteRule ^ - [L]' +echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]' +echo 'RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]' +echo 'RewriteRule . index.php [L]' +)` + ); + } + // WordPress versions below 5.1 didn't use proper spacing in wp-config. const configAnchor = wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' ) diff --git a/packages/env/package.json b/packages/env/package.json index d86d518e41e497..f28345746b5891 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -36,12 +36,12 @@ "wp-env": "bin/wp-env" }, "dependencies": { + "@inquirer/prompts": "^7.2.0", "chalk": "^4.0.0", "copy-dir": "^1.3.0", "docker-compose": "^0.24.3", "extract-zip": "^1.6.7", "got": "^11.8.5", - "inquirer": "^7.1.0", "js-yaml": "^3.13.1", "ora": "^4.0.2", "rimraf": "^5.0.10", diff --git a/packages/escape-html/tsconfig.json b/packages/escape-html/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/escape-html/tsconfig.json +++ b/packages/escape-html/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/eslint-plugin/tsconfig.json b/packages/eslint-plugin/tsconfig.json index e17815f78a6a16..a769c50a12df4c 100644 --- a/packages/eslint-plugin/tsconfig.json +++ b/packages/eslint-plugin/tsconfig.json @@ -3,12 +3,12 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "CommonJS", - "rootDir": "rules", - "declarationDir": "build-types" + "rootDir": "rules" }, "references": [ { "path": "../prettier-config" } ], // NOTE: This package is being progressively typed. You are encouraged to // expand this array with files which can be type-checked. At some point in // the future, this can be simplified to an `includes` of `src/**/*`. - "files": [ "rules/dependency-group.js", "rules/no-unsafe-wp-apis.js" ] + "files": [ "rules/dependency-group.js", "rules/no-unsafe-wp-apis.js" ], + "include": [] } diff --git a/packages/fields/src/actions/permanently-delete-post.tsx b/packages/fields/src/actions/permanently-delete-post.tsx index 688ba5b9918df8..136fcdda9a3e68 100644 --- a/packages/fields/src/actions/permanently-delete-post.tsx +++ b/packages/fields/src/actions/permanently-delete-post.tsx @@ -2,10 +2,19 @@ * WordPress dependencies */ import { store as coreStore } from '@wordpress/core-data'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import type { Action } from '@wordpress/dataviews'; import { trash } from '@wordpress/icons'; +import { useState } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { + Button, + __experimentalText as Text, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -25,93 +34,155 @@ const permanentlyDeletePost: Action< PostWithPermissions > = { const { status, permissions } = item; return status === 'trash' && permissions?.delete; }, - async callback( posts, { registry, onActionPerformed } ) { + hideModalHeader: true, + RenderModal: ( { items, closeModal, onActionPerformed } ) => { + const [ isBusy, setIsBusy ] = useState( false ); const { createSuccessNotice, createErrorNotice } = - registry.dispatch( noticesStore ); - const { deleteEntityRecord } = registry.dispatch( coreStore ); - const promiseResult = await Promise.allSettled( - posts.map( ( post ) => { - return deleteEntityRecord( - 'postType', - post.type, - post.id, - { force: true }, - { throwOnError: true } - ); - } ) + useDispatch( noticesStore ); + const { deleteEntityRecord } = useDispatch( coreStore ); + + return ( + <VStack spacing="5"> + <Text> + { items.length > 1 + ? sprintf( + // translators: %d: number of items to delete. + _n( + 'Are you sure you want to permanently delete %d item?', + 'Are you sure you want to permanently delete %d items?', + items.length + ), + items.length + ) + : sprintf( + // translators: %s: The post's title + __( + 'Are you sure you want to permanently delete "%s"?' + ), + decodeEntities( getItemTitle( items[ 0 ] ) ) + ) } + </Text> + <HStack justify="right"> + <Button + variant="tertiary" + onClick={ closeModal } + disabled={ isBusy } + accessibleWhenDisabled + __next40pxDefaultSize + > + { __( 'Cancel' ) } + </Button> + <Button + variant="primary" + onClick={ async () => { + setIsBusy( true ); + const promiseResult = await Promise.allSettled( + items.map( ( post ) => + deleteEntityRecord( + 'postType', + post.type, + post.id, + { force: true }, + { throwOnError: true } + ) + ) + ); + + // If all the promises were fulfilled with success. + if ( + promiseResult.every( + ( { status } ) => status === 'fulfilled' + ) + ) { + let successMessage; + if ( promiseResult.length === 1 ) { + successMessage = sprintf( + /* translators: The posts's title. */ + __( '"%s" permanently deleted.' ), + getItemTitle( items[ 0 ] ) + ); + } else { + successMessage = __( + 'The items were permanently deleted.' + ); + } + createSuccessNotice( successMessage, { + type: 'snackbar', + id: 'permanently-delete-post-action', + } ); + onActionPerformed?.( items ); + } else { + // If there was at lease one failure. + let errorMessage; + // If we were trying to permanently delete a single post. + if ( promiseResult.length === 1 ) { + const typedError = promiseResult[ 0 ] as { + reason?: CoreDataError; + }; + if ( typedError.reason?.message ) { + errorMessage = + typedError.reason.message; + } else { + errorMessage = __( + 'An error occurred while permanently deleting the item.' + ); + } + // If we were trying to permanently delete multiple posts + } else { + const errorMessages = new Set(); + const failedPromises = promiseResult.filter( + ( { status } ) => status === 'rejected' + ); + for ( const failedPromise of failedPromises ) { + const typedError = failedPromise as { + reason?: CoreDataError; + }; + if ( typedError.reason?.message ) { + errorMessages.add( + typedError.reason.message + ); + } + } + if ( errorMessages.size === 0 ) { + errorMessage = __( + 'An error occurred while permanently deleting the items.' + ); + } else if ( errorMessages.size === 1 ) { + errorMessage = sprintf( + /* translators: %s: an error message */ + __( + 'An error occurred while permanently deleting the items: %s' + ), + [ ...errorMessages ][ 0 ] + ); + } else { + errorMessage = sprintf( + /* translators: %s: a list of comma separated error messages */ + __( + 'Some errors occurred while permanently deleting the items: %s' + ), + [ ...errorMessages ].join( ',' ) + ); + } + } + createErrorNotice( errorMessage, { + type: 'snackbar', + } ); + } + + setIsBusy( false ); + closeModal?.(); + } } + isBusy={ isBusy } + disabled={ isBusy } + accessibleWhenDisabled + __next40pxDefaultSize + > + { __( 'Delete permanently' ) } + </Button> + </HStack> + </VStack> ); - // If all the promises were fulfilled with success. - if ( promiseResult.every( ( { status } ) => status === 'fulfilled' ) ) { - let successMessage; - if ( promiseResult.length === 1 ) { - successMessage = sprintf( - /* translators: The posts's title. */ - __( '"%s" permanently deleted.' ), - getItemTitle( posts[ 0 ] ) - ); - } else { - successMessage = __( 'The items were permanently deleted.' ); - } - createSuccessNotice( successMessage, { - type: 'snackbar', - id: 'permanently-delete-post-action', - } ); - onActionPerformed?.( posts ); - } else { - // If there was at lease one failure. - let errorMessage; - // If we were trying to permanently delete a single post. - if ( promiseResult.length === 1 ) { - const typedError = promiseResult[ 0 ] as { - reason?: CoreDataError; - }; - if ( typedError.reason?.message ) { - errorMessage = typedError.reason.message; - } else { - errorMessage = __( - 'An error occurred while permanently deleting the item.' - ); - } - // If we were trying to permanently delete multiple posts - } else { - const errorMessages = new Set(); - const failedPromises = promiseResult.filter( - ( { status } ) => status === 'rejected' - ); - for ( const failedPromise of failedPromises ) { - const typedError = failedPromise as { - reason?: CoreDataError; - }; - if ( typedError.reason?.message ) { - errorMessages.add( typedError.reason.message ); - } - } - if ( errorMessages.size === 0 ) { - errorMessage = __( - 'An error occurred while permanently deleting the items.' - ); - } else if ( errorMessages.size === 1 ) { - errorMessage = sprintf( - /* translators: %s: an error message */ - __( - 'An error occurred while permanently deleting the items: %s' - ), - [ ...errorMessages ][ 0 ] - ); - } else { - errorMessage = sprintf( - /* translators: %s: a list of comma separated error messages */ - __( - 'Some errors occurred while permanently deleting the items: %s' - ), - [ ...errorMessages ].join( ',' ) - ); - } - } - createErrorNotice( errorMessage, { - type: 'snackbar', - } ); - } }, }; diff --git a/packages/fields/src/components/create-template-part-modal/index.tsx b/packages/fields/src/components/create-template-part-modal/index.tsx index 4043a7824fac49..8728f2681a4936 100644 --- a/packages/fields/src/components/create-template-part-modal/index.tsx +++ b/packages/fields/src/components/create-template-part-modal/index.tsx @@ -5,13 +5,8 @@ import { Icon, BaseControl, TextControl, - Flex, - FlexItem, - FlexBlock, Button, Modal, - __experimentalRadioGroup as RadioGroup, - __experimentalRadio as Radio, __experimentalHStack as HStack, __experimentalVStack as VStack, } from '@wordpress/components'; @@ -40,6 +35,13 @@ import { useExistingTemplateParts, } from './utils'; +function getAreaRadioId( value: string, instanceId: number ) { + return `fields-create-template-part-modal__area-option-${ value }-${ instanceId }`; +} +function getAreaRadioDescriptionId( value: string, instanceId: number ) { + return `fields-create-template-part-modal__area-option-description-${ value }-${ instanceId }`; +} + type CreateTemplatePartModalContentsProps = { defaultArea?: string; blocks: any[]; @@ -201,52 +203,66 @@ export function CreateTemplatePartModalContents( { onChange={ setTitle } required /> - <BaseControl - __nextHasNoMarginBottom - label={ __( 'Area' ) } - id={ `fields-create-template-part-modal__area-selection-${ instanceId }` } - className="fields-create-template-part-modal__area-base-control" - > - <RadioGroup - label={ __( 'Area' ) } - className="fields-create-template-part-modal__area-radio-group" - id={ `fields-create-template-part-modal__area-selection-${ instanceId }` } - onChange={ ( value ) => - value && typeof value === 'string' - ? setArea( value ) - : () => void 0 - } - checked={ area } - > + <fieldset> + <BaseControl.VisualLabel as="legend"> + { __( 'Area' ) } + </BaseControl.VisualLabel> + <div className="fields-create-template-part-modal__area-radio-group"> { ( defaultTemplatePartAreas ?? [] ).map( ( item ) => { const icon = getTemplatePartIcon( item.icon ); return ( - <Radio - __next40pxDefaultSize - key={ item.label } - value={ item.area } - className="fields-create-template-part-modal__area-radio" + <div + key={ item.area } + className="fields-create-template-part-modal__area-radio-wrapper" > - <Flex align="start" justify="start"> - <FlexItem> - <Icon icon={ icon } /> - </FlexItem> - <FlexBlock className="fields-create-template-part-modal__option-label"> - { item.label } - <div>{ item.description }</div> - </FlexBlock> - - <FlexItem className="fields-create-template-part-modal__checkbox"> - { area === item.area && ( - <Icon icon={ check } /> - ) } - </FlexItem> - </Flex> - </Radio> + <input + type="radio" + id={ getAreaRadioId( + item.area, + instanceId + ) } + name={ `fields-create-template-part-modal__area-${ instanceId }` } + value={ item.area } + checked={ area === item.area } + onChange={ () => { + setArea( item.area ); + } } + aria-describedby={ getAreaRadioDescriptionId( + item.area, + instanceId + ) } + /> + <Icon + icon={ icon } + className="fields-create-template-part-modal__area-radio-icon" + /> + <label + htmlFor={ getAreaRadioId( + item.area, + instanceId + ) } + className="fields-create-template-part-modal__area-radio-label" + > + { item.label } + </label> + <Icon + icon={ check } + className="fields-create-template-part-modal__area-radio-checkmark" + /> + <p + className="fields-create-template-part-modal__area-radio-description" + id={ getAreaRadioDescriptionId( + item.area, + instanceId + ) } + > + { item.description } + </p> + </div> ); } ) } - </RadioGroup> - </BaseControl> + </div> + </fieldset> <HStack justify="right"> <Button __next40pxDefaultSize diff --git a/packages/fields/src/components/create-template-part-modal/style.scss b/packages/fields/src/components/create-template-part-modal/style.scss index fedc0326648c2e..bba250b8f3a262 100644 --- a/packages/fields/src/components/create-template-part-modal/style.scss +++ b/packages/fields/src/components/create-template-part-modal/style.scss @@ -3,61 +3,86 @@ } .fields-create-template-part-modal__area-radio-group { - width: 100%; - border: $border-width solid $gray-700; + border: $border-width solid $gray-600; border-radius: $radius-small; +} + +.fields-create-template-part-modal__area-radio-wrapper { + position: relative; + padding: $grid-unit-15; + + display: grid; + align-items: center; + grid-template-columns: min-content 1fr min-content; + grid-gap: $grid-unit-05 $grid-unit-10; + + color: $gray-900; + + & + & { + border-top: $border-width solid $gray-600; + } + + input[type="radio"] { + position: absolute; + opacity: 0; + } + + &:has(input[type="radio"]:checked) { + // This is needed to make sure that the focus ring always renders on top + // of the sibling radio "wrapper"'s borders. + z-index: 1; + } + + &:has(input[type="radio"]:not(:checked)):hover { + color: var(--wp-admin-theme-color); + } + + // Pass-through pointer events, so that the corresponding radio input + // gets checked when clicking on the underlying label + > *:not(.fields-create-template-part-modal__area-radio-label) { + pointer-events: none; + } +} + +.fields-create-template-part-modal__area-radio-label { + // Capture pointer clicks for the whole radio wrapper + &::before { + content: ""; + position: absolute; + inset: 0; + } + + input[type="radio"]:not(:checked) ~ &::before { + cursor: pointer; + } + + input[type="radio"]:focus-visible ~ &::before { + outline: 4px solid transparent; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } +} + +.fields-create-template-part-modal__area-radio-icon, +.fields-create-template-part-modal__area-radio-checkmark { + fill: currentColor; +} + +.fields-create-template-part-modal__area-radio-checkmark { + input[type="radio"]:not(:checked) ~ & { + opacity: 0; + } +} + +.fields-create-template-part-modal__area-radio-description { + grid-column: 2 / 3; + margin: 0; + + color: $gray-700; + font-size: $helptext-font-size; + line-height: normal; + text-wrap: pretty; - .components-button.fields-create-template-part-modal__area-radio { - display: block; - width: 100%; - height: 100%; - text-align: left; - padding: $grid-unit-15; - - &, - &.is-secondary:hover, - &.is-primary:hover { - margin: 0; - background-color: inherit; - border-bottom: $border-width solid $gray-700; - border-radius: 0; - - &:not(:focus) { - box-shadow: none; - } - - &:focus { - border-bottom: $border-width solid $white; - } - - &:last-of-type { - border-bottom: none; - } - } - - &:not(:hover), - &[aria-checked="true"] { - color: $gray-900; - cursor: auto; - - .fields-create-template-part-modal__option-label div { - color: $gray-600; - } - } - - .fields-create-template-part-modal__option-label { - padding-top: $grid-unit-05; - white-space: normal; - - div { - padding-top: $grid-unit-05; - font-size: $helptext-font-size; - } - } - - .fields-create-template-part-modal__checkbox { - margin-left: auto; - min-width: $grid-unit-30; - } + input[type="radio"]:not(:checked):hover ~ & { + color: inherit; } } diff --git a/packages/fields/src/fields/page-title/style.scss b/packages/fields/src/fields/page-title/style.scss deleted file mode 100644 index def56aa466a8a1..00000000000000 --- a/packages/fields/src/fields/page-title/style.scss +++ /dev/null @@ -1,10 +0,0 @@ -.fields-field__page-title__badge { - background: $gray-100; - color: $gray-800; - padding: 0 $grid-unit-05; - border-radius: $radius-small; - font-size: 12px; - font-weight: 400; - flex-shrink: 0; - line-height: $grid-unit-05 * 5; -} diff --git a/packages/fields/src/fields/page-title/view.tsx b/packages/fields/src/fields/page-title/view.tsx index 0be4c16d5d29ae..eb5184362ec82b 100644 --- a/packages/fields/src/fields/page-title/view.tsx +++ b/packages/fields/src/fields/page-title/view.tsx @@ -5,12 +5,15 @@ import { __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import type { Settings } from '@wordpress/core-data'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies */ import type { CommonPost } from '../../types'; import { BaseTitleView } from '../title/view'; +import { unlock } from '../../lock-unlock'; +const { Badge } = unlock( componentsPrivateApis ); export default function PageTitleView( { item }: { item: CommonPost } ) { const { frontPageId, postsPageId } = useSelect( ( select ) => { @@ -27,11 +30,11 @@ export default function PageTitleView( { item }: { item: CommonPost } ) { return ( <BaseTitleView item={ item } className="fields-field__page-title"> { [ frontPageId, postsPageId ].includes( item.id as number ) && ( - <span className="fields-field__page-title__badge"> + <Badge> { item.id === frontPageId ? __( 'Homepage' ) : __( 'Posts Page' ) } - </span> + </Badge> ) } </BaseTitleView> ); diff --git a/packages/fields/src/style.scss b/packages/fields/src/style.scss index d9a571270fbb68..96b1f816de5b61 100644 --- a/packages/fields/src/style.scss +++ b/packages/fields/src/style.scss @@ -3,5 +3,4 @@ @import "./fields/featured-image/style.scss"; @import "./fields/template/style.scss"; @import "./fields/title/style.scss"; -@import "./fields/page-title/style.scss"; @import "./fields/pattern-title/style.scss"; diff --git a/packages/fields/tsconfig.json b/packages/fields/tsconfig.json index 46ac86d48e11ee..552aa73b8e5cce 100644 --- a/packages/fields/tsconfig.json +++ b/packages/fields/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "checkJs": false }, "references": [ @@ -29,6 +27,5 @@ { "path": "../url" }, { "path": "../block-editor" }, { "path": "../warning" } - ], - "include": [ "src" ] + ] } diff --git a/packages/hooks/tsconfig.json b/packages/hooks/tsconfig.json index 9e3edfe0ae443c..f197b56919708b 100644 --- a/packages/hooks/tsconfig.json +++ b/packages/hooks/tsconfig.json @@ -2,9 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ] - }, - "include": [ "src/**/*" ] + } } diff --git a/packages/html-entities/tsconfig.json b/packages/html-entities/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/html-entities/tsconfig.json +++ b/packages/html-entities/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json index f90e327f124d7e..b2186db14f4cc4 100644 --- a/packages/i18n/tsconfig.json +++ b/packages/i18n/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "references": [ { "path": "../hooks" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../hooks" } ] } diff --git a/packages/icons/CHANGELOG.md b/packages/icons/CHANGELOG.md index d622019f1ee783..64c1a58b549caf 100644 --- a/packages/icons/CHANGELOG.md +++ b/packages/icons/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Add new `caution` icon ([#66555](https://github.com/WordPress/gutenberg/pull/66555)). +- Add new `error` icon ([#66555](https://github.com/WordPress/gutenberg/pull/66555)). +- Deprecate `warning` icon and rename to `cautionFilled` ([#67895](https://github.com/WordPress/gutenberg/pull/67895)). + ## 10.14.0 (2024-12-11) ## 10.13.0 (2024-11-27) diff --git a/packages/icons/src/icon/stories/index.story.js b/packages/icons/src/icon/stories/index.story.js index 8cbf65d9f259e9..406f986e6ef5dc 100644 --- a/packages/icons/src/icon/stories/index.story.js +++ b/packages/icons/src/icon/stories/index.story.js @@ -11,7 +11,14 @@ import check from '../../library/check'; import * as icons from '../../'; import keywords from './keywords'; -const { Icon: _Icon, ...availableIcons } = icons; +const { + Icon: _Icon, + + // Deprecated aliases + warning: _warning, + + ...availableIcons +} = icons; const meta = { component: Icon, diff --git a/packages/icons/src/icon/stories/keywords.ts b/packages/icons/src/icon/stories/keywords.ts index 3fd962e047bc1d..4de5ae9a7dae93 100644 --- a/packages/icons/src/icon/stories/keywords.ts +++ b/packages/icons/src/icon/stories/keywords.ts @@ -1,13 +1,15 @@ const keywords: Partial< Record< keyof typeof import('../../'), string[] > > = { cancelCircleFilled: [ 'close' ], + caution: [ 'alert', 'warning' ], + cautionFilled: [ 'alert', 'warning' ], create: [ 'add' ], + error: [ 'alert', 'caution', 'warning' ], file: [ 'folder' ], seen: [ 'show' ], thumbsDown: [ 'dislike' ], thumbsUp: [ 'like' ], trash: [ 'delete' ], unseen: [ 'hide' ], - warning: [ 'alert', 'caution' ], }; export default keywords; diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 14eaec92b78c4d..e82b09e5d5afe9 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -37,6 +37,12 @@ export { default as caption } from './library/caption'; export { default as capturePhoto } from './library/capture-photo'; export { default as captureVideo } from './library/capture-video'; export { default as category } from './library/category'; +export { default as caution } from './library/caution'; +export { + /** @deprecated Import `cautionFilled` instead. */ + default as warning, + default as cautionFilled, +} from './library/caution-filled'; export { default as chartBar } from './library/chart-bar'; export { default as check } from './library/check'; export { default as chevronDown } from './library/chevron-down'; @@ -84,6 +90,7 @@ export { default as download } from './library/download'; export { default as edit } from './library/edit'; export { default as envelope } from './library/envelope'; export { default as external } from './library/external'; +export { default as error } from './library/error'; export { default as file } from './library/file'; export { default as filter } from './library/filter'; export { default as flipHorizontal } from './library/flip-horizontal'; @@ -301,6 +308,5 @@ export { default as update } from './library/update'; export { default as upload } from './library/upload'; export { default as verse } from './library/verse'; export { default as video } from './library/video'; -export { default as warning } from './library/warning'; export { default as widget } from './library/widget'; export { default as wordpress } from './library/wordpress'; diff --git a/packages/icons/src/library/caution-filled.js b/packages/icons/src/library/caution-filled.js new file mode 100644 index 00000000000000..5e7779db85f862 --- /dev/null +++ b/packages/icons/src/library/caution-filled.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const cautionFilled = ( + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Path d="M12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM12.75 8V13H11.25V8H12.75ZM12.75 14.5V16H11.25V14.5H12.75Z" /> + </SVG> +); + +export default cautionFilled; diff --git a/packages/icons/src/library/caution.js b/packages/icons/src/library/caution.js new file mode 100644 index 00000000000000..f6d23fdfc7eddf --- /dev/null +++ b/packages/icons/src/library/caution.js @@ -0,0 +1,16 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const caution = ( + <SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <Path + fillRule="evenodd" + clipRule="evenodd" + d="M5.5 12a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 12v-1.5h1.5V16h-1.5Zm0-8v5h1.5V8h-1.5Z" + /> + </SVG> +); + +export default caution; diff --git a/packages/icons/src/library/error.js b/packages/icons/src/library/error.js new file mode 100644 index 00000000000000..2dc2bccbf639ce --- /dev/null +++ b/packages/icons/src/library/error.js @@ -0,0 +1,16 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const error = ( + <SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <Path + fillRule="evenodd" + clipRule="evenodd" + d="M12.218 5.377a.25.25 0 0 0-.436 0l-7.29 12.96a.25.25 0 0 0 .218.373h14.58a.25.25 0 0 0 .218-.372l-7.29-12.96Zm-1.743-.735c.669-1.19 2.381-1.19 3.05 0l7.29 12.96a1.75 1.75 0 0 1-1.525 2.608H4.71a1.75 1.75 0 0 1-1.525-2.608l7.29-12.96ZM12.75 17.46h-1.5v-1.5h1.5v1.5Zm-1.5-3h1.5v-5h-1.5v5Z" + /> + </SVG> +); + +export default error; diff --git a/packages/icons/src/library/info.js b/packages/icons/src/library/info.js index f3425d9e950415..24d41d798263f7 100644 --- a/packages/icons/src/library/info.js +++ b/packages/icons/src/library/info.js @@ -4,8 +4,12 @@ import { SVG, Path } from '@wordpress/primitives'; const info = ( - <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> - <Path d="M12 3.2c-4.8 0-8.8 3.9-8.8 8.8 0 4.8 3.9 8.8 8.8 8.8 4.8 0 8.8-3.9 8.8-8.8 0-4.8-4-8.8-8.8-8.8zm0 16c-4 0-7.2-3.3-7.2-7.2C4.8 8 8 4.8 12 4.8s7.2 3.3 7.2 7.2c0 4-3.2 7.2-7.2 7.2zM11 17h2v-6h-2v6zm0-8h2V7h-2v2z" /> + <SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <Path + fillRule="evenodd" + clipRule="evenodd" + d="M5.5 12a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm.75 4v1.5h-1.5V8h1.5Zm0 8v-5h-1.5v5h1.5Z" + /> </SVG> ); diff --git a/packages/icons/src/library/warning.js b/packages/icons/src/library/warning.js deleted file mode 100644 index 97086c5c9292bd..00000000000000 --- a/packages/icons/src/library/warning.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * WordPress dependencies - */ -import { SVG, Path } from '@wordpress/primitives'; - -const warning = ( - <SVG xmlns="http://www.w3.org/2000/svg" viewBox="-2 -2 24 24"> - <Path d="M10 2c4.42 0 8 3.58 8 8s-3.58 8-8 8-8-3.58-8-8 3.58-8 8-8zm1.13 9.38l.35-6.46H8.52l.35 6.46h2.26zm-.09 3.36c.24-.23.37-.55.37-.96 0-.42-.12-.74-.36-.97s-.59-.35-1.06-.35-.82.12-1.07.35-.37.55-.37.97c0 .41.13.73.38.96.26.23.61.34 1.06.34s.8-.11 1.05-.34z" /> - </SVG> -); - -export default warning; diff --git a/packages/icons/tsconfig.json b/packages/icons/tsconfig.json index 2877b1d31633bd..75638a3b50a790 100644 --- a/packages/icons/tsconfig.json +++ b/packages/icons/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ], "references": [ { "path": "../element" }, { "path": "../primitives" } ] } diff --git a/packages/interactivity-router/src/assets/styles.ts b/packages/interactivity-router/src/assets/styles.ts new file mode 100644 index 00000000000000..ddb41eabc7a758 --- /dev/null +++ b/packages/interactivity-router/src/assets/styles.ts @@ -0,0 +1,79 @@ +const cssUrlRegEx = + /url\(\s*(?:(["'])((?:\\.|[^\n\\"'])+)\1|((?:\\.|[^\s,"'()\\])+))\s*\)/g; + +const resolveUrl = ( relativeUrl: string, baseUrl: string ) => { + try { + return new URL( relativeUrl, baseUrl ).toString(); + } catch ( e ) { + return relativeUrl; + } +}; + +const withAbsoluteUrls = ( cssText: string, baseUrl: string ) => + cssText.replace( + cssUrlRegEx, + ( _match, quotes = '', relUrl1, relUrl2 ) => + `url(${ quotes }${ resolveUrl( + relUrl1 || relUrl2, + baseUrl + ) }${ quotes })` + ); + +const styleSheetCache = new Map< string, Promise< CSSStyleSheet > >(); + +const getCachedSheet = async ( + sheetId: string, + factory: () => Promise< CSSStyleSheet > +) => { + if ( ! styleSheetCache.has( sheetId ) ) { + styleSheetCache.set( sheetId, factory() ); + } + return styleSheetCache.get( sheetId ); +}; + +const sheetFromLink = async ( + { id, href, sheet: elementSheet }: HTMLLinkElement, + baseUrl: string +) => { + const sheetId = id || href; + const sheetUrl = resolveUrl( href, baseUrl ); + + if ( elementSheet ) { + return getCachedSheet( sheetId, () => { + const sheet = new CSSStyleSheet(); + for ( const { cssText } of elementSheet.cssRules ) { + sheet.insertRule( withAbsoluteUrls( cssText, sheetUrl ) ); + } + return Promise.resolve( sheet ); + } ); + } + return getCachedSheet( sheetId, async () => { + const response = await fetch( href ); + const text = await response.text(); + const sheet = new CSSStyleSheet(); + await sheet.replace( withAbsoluteUrls( text, sheetUrl ) ); + return sheet; + } ); +}; + +const sheetFromStyle = async ( { textContent }: HTMLStyleElement ) => { + const sheetId = textContent; + return getCachedSheet( sheetId, async () => { + const sheet = new CSSStyleSheet(); + await sheet.replace( textContent ); + return sheet; + } ); +}; + +export const generateCSSStyleSheets = ( + doc: Document, + baseUrl: string = ( doc.location || window.location ).href +): Promise< CSSStyleSheet >[] => + [ ...doc.querySelectorAll( 'style,link[rel=stylesheet]' ) ].map( + ( element ) => { + if ( 'LINK' === element.nodeName ) { + return sheetFromLink( element as HTMLLinkElement, baseUrl ); + } + return sheetFromStyle( element as HTMLStyleElement ); + } + ); diff --git a/packages/interactivity-router/src/head.ts b/packages/interactivity-router/src/head.ts deleted file mode 100644 index 69139348b582ff..00000000000000 --- a/packages/interactivity-router/src/head.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * The cache of prefetched stylesheets and scripts. - */ -export const headElements = new Map< - string, - { tag: HTMLElement; text?: string } ->(); - -/** - * Helper to update only the necessary tags in the head. - * - * @async - * @param newHead The head elements of the new page. - */ -export const updateHead = async ( newHead: HTMLHeadElement[] ) => { - // Helper to get the tag id store in the cache. - const getTagId = ( tag: Element ) => tag.id || tag.outerHTML; - - // Map incoming head tags by their content. - const newHeadMap = new Map< string, Element >(); - for ( const child of newHead ) { - newHeadMap.set( getTagId( child ), child ); - } - - const toRemove: Element[] = []; - - // Detect nodes that should be added or removed. - for ( const child of document.head.children ) { - const id = getTagId( child ); - // Always remove styles and links as they might change. - if ( child.nodeName === 'LINK' || child.nodeName === 'STYLE' ) { - toRemove.push( child ); - } else if ( newHeadMap.has( id ) ) { - newHeadMap.delete( id ); - } else if ( child.nodeName !== 'SCRIPT' && child.nodeName !== 'META' ) { - toRemove.push( child ); - } - } - - await Promise.all( - [ ...headElements.entries() ] - .filter( ( [ , { tag } ] ) => tag.nodeName === 'SCRIPT' ) - .map( async ( [ url ] ) => { - await import( /* webpackIgnore: true */ url ); - } ) - ); - - // Prepare new assets. - const toAppend = [ ...newHeadMap.values() ]; - - // Apply the changes. - toRemove.forEach( ( n ) => n.remove() ); - document.head.append( ...toAppend ); -}; - -/** - * Fetches and processes head assets (stylesheets and scripts) from a specified document. - * - * @async - * @param doc The document from which to fetch head assets. It should support standard DOM querying methods. - * - * @return Returns an array of HTML elements representing the head assets. - */ -export const fetchHeadAssets = async ( - doc: Document -): Promise< HTMLElement[] > => { - const headTags = []; - - // We only want to fetch module scripts because regular scripts (without - // `async` or `defer` attributes) can depend on the execution of other scripts. - // Scripts found in the head are blocking and must be executed in order. - const scripts = doc.querySelectorAll< HTMLScriptElement >( - 'script[type="module"][src]' - ); - - scripts.forEach( ( script ) => { - const src = script.getAttribute( 'src' ); - if ( ! headElements.has( src ) ) { - // add the <link> elements to prefetch the module scripts - const link = doc.createElement( 'link' ); - link.rel = 'modulepreload'; - link.href = src; - document.head.append( link ); - headElements.set( src, { tag: script } ); - } - } ); - - const stylesheets = doc.querySelectorAll< HTMLLinkElement >( - 'link[rel=stylesheet]' - ); - - await Promise.all( - Array.from( stylesheets ).map( async ( tag ) => { - const href = tag.getAttribute( 'href' ); - if ( ! href ) { - return; - } - - if ( ! headElements.has( href ) ) { - try { - const response = await fetch( href ); - const text = await response.text(); - headElements.set( href, { - tag, - text, - } ); - } catch ( e ) { - // eslint-disable-next-line no-console - console.error( e ); - } - } - - const headElement = headElements.get( href ); - const styleElement = doc.createElement( 'style' ); - styleElement.textContent = headElement.text; - - headTags.push( styleElement ); - } ) - ); - - return [ - doc.querySelector( 'title' ), - ...doc.querySelectorAll( 'style' ), - ...headTags, - ]; -}; diff --git a/packages/interactivity-router/src/index.ts b/packages/interactivity-router/src/index.ts index 0c10e896ce1ef5..ded21d35dd5886 100644 --- a/packages/interactivity-router/src/index.ts +++ b/packages/interactivity-router/src/index.ts @@ -6,7 +6,7 @@ import { store, privateApis, getConfig } from '@wordpress/interactivity'; /** * Internal dependencies */ -import { fetchHeadAssets, updateHead, headElements } from './head'; +import { generateCSSStyleSheets } from './assets/styles'; const { directivePrefix, @@ -37,16 +37,18 @@ interface PrefetchOptions { interface VdomParams { vdom?: typeof initialVdom; + baseUrl?: string; } interface Page { regions: Record< string, any >; - head: HTMLHeadElement[]; + styles: Promise< CSSStyleSheet >[]; + scriptModules: string[]; title: string; initialData: any; } -type RegionsToVdom = ( dom: Document, params?: VdomParams ) => Promise< Page >; +type RegionsToVdom = ( dom: Document, params?: VdomParams ) => Page; // Check if the navigation mode is full page or region based. const navigationMode: 'regionBased' | 'fullPage' = @@ -73,7 +75,7 @@ const fetchPage = async ( url: string, { html }: { html: string } ) => { html = await res.text(); } const dom = new window.DOMParser().parseFromString( html, 'text/html' ); - return regionsToVdom( dom ); + return regionsToVdom( dom, { baseUrl: url } ); } catch ( e ) { return false; } @@ -81,12 +83,17 @@ const fetchPage = async ( url: string, { html }: { html: string } ) => { // Return an object with VDOM trees of those HTML regions marked with a // `router-region` directive. -const regionsToVdom: RegionsToVdom = async ( dom, { vdom } = {} ) => { +const regionsToVdom: RegionsToVdom = ( dom, { vdom, baseUrl } = {} ) => { const regions = { body: undefined }; - let head: HTMLElement[]; + const styles = generateCSSStyleSheets( dom, baseUrl ); + const scriptModules = [ + ...dom.querySelectorAll< HTMLScriptElement >( + 'script[type=module][src]' + ), + ].map( ( s ) => s.src ); + if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( navigationMode === 'fullPage' ) { - head = await fetchHeadAssets( dom ); regions.body = vdom ? vdom.get( document.body ) : toVdom( dom.body ); @@ -103,15 +110,28 @@ const regionsToVdom: RegionsToVdom = async ( dom, { vdom } = {} ) => { } const title = dom.querySelector( 'title' )?.innerText; const initialData = parseServerData( dom ); - return { regions, head, title, initialData }; + return { regions, styles, scriptModules, title, initialData }; }; // Render all interactive regions contained in the given page. const renderRegions = async ( page: Page ) => { + // Wait for styles and modules to be ready. + await Promise.all( [ + ...page.styles, + ...page.scriptModules.map( + ( src ) => import( /* webpackIgnore: true */ src ) + ), + ] ); + // Replace style sheets. + const sheets = await Promise.all( page.styles ); + window.document + .querySelectorAll( 'style,link[rel=stylesheet]' ) + .forEach( ( element ) => element.remove() ); + window.document.adoptedStyleSheets = sheets; + if ( globalThis.IS_GUTENBERG_PLUGIN ) { if ( navigationMode === 'fullPage' ) { - // Once this code is tested and more mature, the head should be updated for region based navigation as well. - await updateHead( page.head ); + // Update HTML. const fragment = getRegionRootFragment( document.body ); batch( () => { populateServerData( page.initialData ); @@ -169,23 +189,14 @@ window.addEventListener( 'popstate', async () => { // Initialize the router and cache the initial page using the initial vDOM. // Once this code is tested and more mature, the head should be updated for // region based navigation as well. -if ( globalThis.IS_GUTENBERG_PLUGIN ) { - if ( navigationMode === 'fullPage' ) { - // Cache the scripts. Has to be called before fetching the assets. - [].map.call( - document.querySelectorAll( 'script[type="module"][src]' ), - ( script ) => { - headElements.set( script.getAttribute( 'src' ), { - tag: script, - } ); - } - ); - await fetchHeadAssets( document ); - } -} pages.set( getPagePath( window.location.href ), - Promise.resolve( regionsToVdom( document, { vdom: initialVdom } ) ) + Promise.resolve( + regionsToVdom( document, { + vdom: initialVdom, + baseUrl: window.location.href, + } ) + ) ); // Check if the link is valid for client-side navigation. diff --git a/packages/interactivity-router/tsconfig.json b/packages/interactivity-router/tsconfig.json index f601a26a86f5f4..616718560d02cc 100644 --- a/packages/interactivity-router/tsconfig.json +++ b/packages/interactivity-router/tsconfig.json @@ -2,11 +2,8 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "checkJs": false, "strict": false }, - "references": [ { "path": "../a11y" }, { "path": "../interactivity" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../a11y" }, { "path": "../interactivity" } ] } diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 6b32e4ec35a978..ec54cdfd7c0367 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Enhancements + +- Allow more iterables to be used in each directives ([#67798](https://github.com/WordPress/gutenberg/pull/67798)). + +### Bug Fixes + +- Fix an error when the value used in an each directive is not iterable ([#67798](https://github.com/WordPress/gutenberg/pull/67798)). + ## 6.14.0 (2024-12-11) ## 6.13.0 (2024-11-27) diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx index 31e07d095e0a4c..bddd017b1c99db 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -4,7 +4,7 @@ /** * External dependencies */ -import { h as createElement, type RefObject } from 'preact'; +import { h as createElement, type VNode, type RefObject } from 'preact'; import { useContext, useMemo, useRef } from 'preact/hooks'; /** @@ -567,11 +567,19 @@ export default () => { const [ entry ] = each; const { namespace } = entry; - const list = evaluate( entry ); + const iterable = evaluate( entry ); + + if ( typeof iterable?.[ Symbol.iterator ] !== 'function' ) { + return; + } + const itemProp = isNonDefaultDirectiveSuffix( entry ) ? kebabToCamelCase( entry.suffix ) : 'item'; - return list.map( ( item ) => { + + const result: VNode< any >[] = []; + + for ( const item of iterable ) { const itemContext = proxifyContext( proxifyState( namespace, {} ), inheritedValue.client[ namespace ] @@ -596,12 +604,15 @@ export default () => { ? getEvaluate( { scope } )( eachKey[ 0 ] ) : item; - return createElement( - Provider, - { value: mergedContext, key }, - element.props.content + result.push( + createElement( + Provider, + { value: mergedContext, key }, + element.props.content + ) ); - } ); + } + return result; }, { priority: 20 } ); diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index e9b9f48ba3518e..7899e3eafd2281 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -77,7 +77,7 @@ interface DirectiveArgs { } export interface DirectiveCallback { - ( args: DirectiveArgs ): VNode< any > | null | void; + ( args: DirectiveArgs ): VNode< any > | VNode< any >[] | null | void; } interface DirectiveOptions { diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json index 1d154e2089065d..a4d86e65fa1dd6 100644 --- a/packages/interactivity/tsconfig.json +++ b/packages/interactivity/tsconfig.json @@ -2,10 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", - "noImplicitAny": false - }, - "include": [ "src/**/*" ] + } } diff --git a/packages/interactivity/tsconfig.test.json b/packages/interactivity/tsconfig.test.json index 6a90abc2ba2210..ad6813d6fec0ff 100644 --- a/packages/interactivity/tsconfig.test.json +++ b/packages/interactivity/tsconfig.test.json @@ -2,12 +2,12 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", "noEmit": true, "emitDeclarationOnly": false, "types": [ "jest" ] }, "references": [ { "path": "./tsconfig.json" } ], "files": [ "src/test/store.ts" ], + "include": [], "exclude": [] } diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index a0ed9cd83525cc..172d70b09fad31 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Changes + +- `ActionItem.Slot`: Render as `MenuGroup` by default ([#67985](https://github.com/WordPress/gutenberg/pull/67985)). + ## 8.3.0 (2024-12-11) ## 8.2.0 (2024-11-27) diff --git a/packages/interface/src/components/action-item/README.md b/packages/interface/src/components/action-item/README.md index 15c627adfd3296..5611e044c8a985 100644 --- a/packages/interface/src/components/action-item/README.md +++ b/packages/interface/src/components/action-item/README.md @@ -24,11 +24,11 @@ Property used to change the event bubbling behavior, passed to the `Slot` compon ### as -The component used as the container of the fills. Defaults to the `ButtonGroup` component. +The component used as the container of the fills. Defaults to the `MenuGroup` component. - Type: `Component` - Required: no -- Default: `ButtonGroup` +- Default: `MenuGroup` ## ActionItem diff --git a/packages/interface/src/components/action-item/index.js b/packages/interface/src/components/action-item/index.js index 4bd5a11e8d71f8..2f3fdd6d3ca301 100644 --- a/packages/interface/src/components/action-item/index.js +++ b/packages/interface/src/components/action-item/index.js @@ -1,14 +1,14 @@ /** * WordPress dependencies */ -import { ButtonGroup, Button, Slot, Fill } from '@wordpress/components'; +import { MenuGroup, Button, Slot, Fill } from '@wordpress/components'; import { Children } from '@wordpress/element'; const noop = () => {}; function ActionItemSlot( { name, - as: Component = ButtonGroup, + as: Component = MenuGroup, fillProps = {}, bubblesVirtually, ...props diff --git a/packages/is-shallow-equal/tsconfig.json b/packages/is-shallow-equal/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/is-shallow-equal/tsconfig.json +++ b/packages/is-shallow-equal/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/keycodes/tsconfig.json b/packages/keycodes/tsconfig.json index be13213c265f05..9534c034fa89e7 100644 --- a/packages/keycodes/tsconfig.json +++ b/packages/keycodes/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "references": [ { "path": "../i18n" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../i18n" } ] } diff --git a/packages/lazy-import/tsconfig.json b/packages/lazy-import/tsconfig.json index 3bf8bde807404d..9647e449d35454 100644 --- a/packages/lazy-import/tsconfig.json +++ b/packages/lazy-import/tsconfig.json @@ -3,8 +3,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "lib", - "declarationDir": "build-types", "useUnknownInCatchVariables": false }, - "include": [ "lib/**/*" ] + "include": [ "lib" ] } diff --git a/packages/media-utils/src/utils/types.ts b/packages/media-utils/src/utils/types.ts index c91d4c67cfc466..c4c6882ea2532e 100644 --- a/packages/media-utils/src/utils/types.ts +++ b/packages/media-utils/src/utils/types.ts @@ -199,7 +199,6 @@ export type Attachment = BetterOmit< }; export type OnChangeHandler = ( attachments: Partial< Attachment >[] ) => void; -export type OnSuccessHandler = ( attachments: Partial< Attachment >[] ) => void; export type OnErrorHandler = ( error: Error ) => void; export type CreateRestAttachment = Partial< RestAttachment >; diff --git a/packages/media-utils/src/utils/upload-media.ts b/packages/media-utils/src/utils/upload-media.ts index 1bc861cfb3b607..ff3f718076512b 100644 --- a/packages/media-utils/src/utils/upload-media.ts +++ b/packages/media-utils/src/utils/upload-media.ts @@ -12,7 +12,6 @@ import type { Attachment, OnChangeHandler, OnErrorHandler, - OnSuccessHandler, } from './types'; import { uploadToServer } from './upload-to-server'; import { validateMimeType } from './validate-mime-type'; @@ -20,6 +19,12 @@ import { validateMimeTypeForUser } from './validate-mime-type-for-user'; import { validateFileSize } from './validate-file-size'; import { UploadError } from './upload-error'; +declare global { + interface Window { + __experimentalMediaProcessing?: boolean; + } +} + interface UploadMediaArgs { // Additional data to include in the request. additionalData?: AdditionalData; @@ -33,8 +38,6 @@ interface UploadMediaArgs { onError?: OnErrorHandler; // Function called each time a file or a temporary representation of the file is available. onFileChange?: OnChangeHandler; - // Function called once a file has completely finished uploading, including thumbnails. - onSuccess?: OnSuccessHandler; // List of allowed mime types and file extensions. wpAllowedMimeTypes?: Record< string, string > | null; // Abort signal. @@ -69,8 +72,11 @@ export function uploadMedia( { const filesSet: Array< Partial< Attachment > | null > = []; const setAndUpdateFiles = ( index: number, value: Attachment | null ) => { - if ( filesSet[ index ]?.url ) { - revokeBlobURL( filesSet[ index ].url ); + // For client-side media processing, this is handled by the upload-media package. + if ( ! window.__experimentalMediaProcessing ) { + if ( filesSet[ index ]?.url ) { + revokeBlobURL( filesSet[ index ].url ); + } } filesSet[ index ] = value; onFileChange?.( @@ -107,10 +113,13 @@ export function uploadMedia( { validFiles.push( mediaFile ); - // Set temporary URL to create placeholder media file, this is replaced - // with final file from media gallery when upload is `done` below. - filesSet.push( { url: createBlobURL( mediaFile ) } ); - onFileChange?.( filesSet as Array< Partial< Attachment > > ); + // For client-side media processing, this is handled by the upload-media package. + if ( ! window.__experimentalMediaProcessing ) { + // Set temporary URL to create placeholder media file, this is replaced + // with final file from media gallery when upload is `done` below. + filesSet.push( { url: createBlobURL( mediaFile ) } ); + onFileChange?.( filesSet as Array< Partial< Attachment > > ); + } } validFiles.map( async ( file, index ) => { diff --git a/packages/media-utils/tsconfig.json b/packages/media-utils/tsconfig.json index ca3e93c2dee668..380e55bc58ff09 100644 --- a/packages/media-utils/tsconfig.json +++ b/packages/media-utils/tsconfig.json @@ -2,12 +2,9 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ], "checkJs": false }, - "include": [ "src/**/*" ], "references": [ { "path": "../api-fetch" }, { "path": "../blob" }, diff --git a/packages/notices/tsconfig.json b/packages/notices/tsconfig.json index e36a6fe9f4eb6b..9c48147297764e 100644 --- a/packages/notices/tsconfig.json +++ b/packages/notices/tsconfig.json @@ -2,11 +2,8 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ], "checkJs": false }, - "references": [ { "path": "../a11y" }, { "path": "../data" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../a11y" }, { "path": "../data" } ] } diff --git a/packages/plugins/tsconfig.json b/packages/plugins/tsconfig.json index 47c626f9ddedc8..66fb760f8896d0 100644 --- a/packages/plugins/tsconfig.json +++ b/packages/plugins/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ] }, "references": [ @@ -14,6 +12,5 @@ { "path": "../hooks" }, { "path": "../icons" }, { "path": "../is-shallow-equal" } - ], - "include": [ "src/**/*" ] + ] } diff --git a/packages/postcss-plugins-preset/CHANGELOG.md b/packages/postcss-plugins-preset/CHANGELOG.md index b1425332402f50..f0e18614b0bdc7 100644 --- a/packages/postcss-plugins-preset/CHANGELOG.md +++ b/packages/postcss-plugins-preset/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- The bundled `autoprefixer` dependency has been updated from requiring `^10.2.5` to requiring `^10.4.20` (see [#68237](https://github.com/WordPress/gutenberg/pull/68237)). + ## 5.14.0 (2024-12-11) ## 5.13.0 (2024-11-27) diff --git a/packages/postcss-plugins-preset/package.json b/packages/postcss-plugins-preset/package.json index caecfb0939863e..aba3012f4cd6bb 100644 --- a/packages/postcss-plugins-preset/package.json +++ b/packages/postcss-plugins-preset/package.json @@ -31,7 +31,7 @@ "main": "lib/index.js", "dependencies": { "@wordpress/base-styles": "*", - "autoprefixer": "^10.2.5" + "autoprefixer": "^10.4.20" }, "peerDependencies": { "postcss": "^8.0.0" diff --git a/packages/prettier-config/tsconfig.json b/packages/prettier-config/tsconfig.json index 0636ff7d0081dd..7899aeee7dfbc9 100644 --- a/packages/prettier-config/tsconfig.json +++ b/packages/prettier-config/tsconfig.json @@ -3,8 +3,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "lib", - "declarationDir": "build-types", "types": [ "node" ] }, - "include": [ "lib/**/*" ] + "include": [ "lib" ] } diff --git a/packages/primitives/tsconfig.json b/packages/primitives/tsconfig.json index 59a95359b5ea65..5dea3e64597b43 100644 --- a/packages/primitives/tsconfig.json +++ b/packages/primitives/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ], "references": [ { "path": "../element" } ] } diff --git a/packages/priority-queue/tsconfig.json b/packages/priority-queue/tsconfig.json index 96d649eb7a6233..2a790d65e67612 100644 --- a/packages/priority-queue/tsconfig.json +++ b/packages/priority-queue/tsconfig.json @@ -2,9 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "requestidlecallback" ] - }, - "include": [ "src/**/*" ] + } } diff --git a/packages/private-apis/src/implementation.ts b/packages/private-apis/src/implementation.ts index 5a5fb3f39fa183..1ac08a71550ff1 100644 --- a/packages/private-apis/src/implementation.ts +++ b/packages/private-apis/src/implementation.ts @@ -32,6 +32,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/dataviews', '@wordpress/fields', '@wordpress/media-utils', + '@wordpress/upload-media', ]; /** diff --git a/packages/private-apis/tsconfig.json b/packages/private-apis/tsconfig.json index 9e3edfe0ae443c..f197b56919708b 100644 --- a/packages/private-apis/tsconfig.json +++ b/packages/private-apis/tsconfig.json @@ -2,9 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ] - }, - "include": [ "src/**/*" ] + } } diff --git a/packages/project-management-automation/tsconfig.json b/packages/project-management-automation/tsconfig.json index 0636ff7d0081dd..7899aeee7dfbc9 100644 --- a/packages/project-management-automation/tsconfig.json +++ b/packages/project-management-automation/tsconfig.json @@ -3,8 +3,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "lib", - "declarationDir": "build-types", "types": [ "node" ] }, - "include": [ "lib/**/*" ] + "include": [ "lib" ] } diff --git a/packages/react-i18n/tsconfig.json b/packages/react-i18n/tsconfig.json index e8e7f164f89a34..32b019421ed3d5 100644 --- a/packages/react-i18n/tsconfig.json +++ b/packages/react-i18n/tsconfig.json @@ -1,10 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "references": [ { "path": "../element" }, { "path": "../i18n" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../element" }, { "path": "../i18n" } ] } diff --git a/packages/react-native-editor/ios/Gemfile.lock b/packages/react-native-editor/ios/Gemfile.lock index e8fbb72a6ca659..4884203a2a00d5 100644 --- a/packages/react-native-editor/ios/Gemfile.lock +++ b/packages/react-native-editor/ios/Gemfile.lock @@ -72,21 +72,19 @@ GEM nap (1.1.0) netrc (0.11.0) public_suffix (4.0.7) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.3.9) ruby-macho (2.5.1) - strscan (3.1.0) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.24.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.6, < 4.0) zeitwerk (2.6.11) PLATFORMS diff --git a/packages/redux-routine/tsconfig.json b/packages/redux-routine/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/redux-routine/tsconfig.json +++ b/packages/redux-routine/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/report-flaky-tests/tsconfig.json b/packages/report-flaky-tests/tsconfig.json index 09fc242db010db..26fcd6f5e51c6f 100644 --- a/packages/report-flaky-tests/tsconfig.json +++ b/packages/report-flaky-tests/tsconfig.json @@ -3,10 +3,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "module": "CommonJS", - "declarationDir": "build-types", - "rootDir": "src", "types": [ "jest" ] }, - "include": [ "src/**/*" ], - "exclude": [ "src/__tests__/**/*", "src/__fixtures__/**/*" ] + "exclude": [ "src/__tests__", "src/__fixtures__" ] } diff --git a/packages/rich-text/src/store/selectors.js b/packages/rich-text/src/store/selectors.js index df87c6a99211a2..16572e301c1dba 100644 --- a/packages/rich-text/src/store/selectors.js +++ b/packages/rich-text/src/store/selectors.js @@ -75,7 +75,7 @@ export const getFormatTypes = createSelector( * }; * ``` * - * @return {Object?} Format type. + * @return {?Object} Format type. */ export function getFormatType( state, name ) { return state.formatTypes[ name ]; diff --git a/packages/rich-text/tsconfig.json b/packages/rich-text/tsconfig.json index 57fe0ae604215f..5dadcb0ed0045c 100644 --- a/packages/rich-text/tsconfig.json +++ b/packages/rich-text/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ], "checkJs": false }, @@ -16,6 +14,5 @@ { "path": "../escape-html" }, { "path": "../i18n" }, { "path": "../keycodes" } - ], - "include": [ "src/**/*" ] + ] } diff --git a/packages/router/tsconfig.json b/packages/router/tsconfig.json index 8706b546ff304d..7d9ba227795ad6 100644 --- a/packages/router/tsconfig.json +++ b/packages/router/tsconfig.json @@ -2,8 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ] }, "references": [ @@ -11,6 +9,5 @@ { "path": "../element" }, { "path": "../private-apis" }, { "path": "../url" } - ], - "include": [ "src/**/*" ] + ] } diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 42afdd02e0d605..7ed1d2439d2e12 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +### Enhancements + +- Recommend listing JavaScript entry points as paths passed to the `start` and `build` commands ([#68251](https://github.com/WordPress/gutenberg/pull/68251)). +- Introduce a new option `--source-path` to customize the source directory used with the `start` and `build` commands ([#68251](https://github.com/WordPress/gutenberg/pull/68251)). + +### Internal + +- The bundled `rtlcss-webpack-plugin` dependency has been replaced with a modified fork of the plugin to fix issues with the original package ([#68201](https://github.com/WordPress/gutenberg/pull/68201)). +- The bundled `sass` dependency has been updated from `^1.50.0` to `^1.54.0` ([#68380](https://github.com/WordPress/gutenberg/pull/68380)). + ## 30.7.0 (2024-12-11) ### Internal diff --git a/packages/scripts/README.md b/packages/scripts/README.md index f86a4c6091c408..aaf4e03d8a0605 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -46,10 +46,6 @@ _Example:_ It might also be a good idea to get familiar with the [JavaScript Build Setup tutorial](https://github.com/WordPress/gutenberg/tree/HEAD/docs/how-to-guides/javascript/js-build-setup.md) for setting up a development environment to use ESNext syntax. It gives a very in-depth explanation of how to use the [build](#build) and [start](#start) scripts. -## Automatic block.json detection and the source code directory - -When using the `start` or `build` commands, the source code directory ( the default is `./src`) and its subdirectories are scanned for the existence of `block.json` files. If one or more are found, they are treated a entry points and will be output into corresponding folders in the `build` directory. This allows for the creation of multiple blocks that use a single build process. The source directory can be customized using the `--webpack-src-dir` flag and the output directory with the `--output-path` flag. - ## Updating to New Release To update an existing project to a new version of `@wordpress/scripts`, open the [changelog](https://github.com/WordPress/gutenberg/blob/HEAD/packages/scripts/CHANGELOG.md), find the version you’re currently on (check `package.json` in the top-level directory of your project), and apply the migration instructions for the newer versions. @@ -66,19 +62,7 @@ Transforms your code according the configuration provided so it’s ready for pr _This script exits after producing a single build. For incremental builds, better suited for development, see the [start](#start) script._ -The entry points for your project get detected by scanning all script fields in `block.json` files located in the `src` directory. The script fields in `block.json` should pass relative paths to `block.json` in the same folder. - -_Example:_ - -```json -{ - "editorScript": "file:index.js", - "script": "file:script.js", - "viewScript": "file:view.js" -} -``` - -The fallback entry point is `src/index.js` (other supported extensions: `.jsx`, `.ts`, and `.tsx`) in case there is no `block.json` file found. In that scenario, the output generated will be written to `build/index.js`. +#### Usage _Example:_ @@ -88,7 +72,7 @@ _Example:_ "build": "wp-scripts build", "build:custom": "wp-scripts build entry-one.js entry-two.js --output-path=custom", "build:copy-php": "wp-scripts build --webpack-copy-php", - "build:custom-directory": "wp-scripts build --webpack-src-dir=custom-directory" + "build:custom-directory": "wp-scripts build --source-path=custom-directory" } } ``` @@ -104,20 +88,21 @@ This script automatically use the optimized config but sometimes you may want to - `--webpack-bundle-analyzer` – enables visualization for the size of webpack output files with an interactive zoomable treemap. - `--webpack-copy-php` – enables copying all PHP files from the source directory ( default is `src` ) and its subfolders to the output directory. -- `--webpack-no-externals` – disables scripts' assets generation, and omits the list of default externals. -- `--webpack-src-dir` – Allows customization of the source code directory. Default is `src`. -- `--output-path` – Allows customization of the output directory. Default is `build`. +- `--webpack-no-externals` – disables scripts’ assets generation, and omits the list of default externals. +- `--source-path` – allows customization of the source directory. The default is the project root `.` when [entry points are listed](#listing-entry-points) in the command, or `src` otherwise. +- `--output-path` – allows customization of the output directory. The default is the `build` folder. Experimental support for the block.json `viewScriptModule` field is available via the `--experimental-modules` option. With this option enabled, script and module fields will all be compiled. The `viewScriptModule` field is analogous to the `viewScript` field, but will compile a module and should be registered in WordPress using the Modules API. +Learn more about [using build scripts](#using-build-scripts) to optimize the development experience based on your specific needs. + #### 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. - ### `build-blocks-manifest` This script generates a PHP file containing block metadata from all @@ -128,10 +113,12 @@ when registering multiple block types, as it allows you to use Usage: `wp-scripts build-blocks-manifest [options]` Options: -- `--input`: Specify the input directory (default: 'build') -- `--output`: Specify the output file path (default: 'build/blocks-manifest.php') + +- `--input`: Specify the input directory (default: 'build') +- `--output`: Specify the output file path (default: 'build/blocks-manifest.php') Example: + ```bash wp-scripts build-blocks-manifest --input=src --output=dist/blocks-manifest.php ``` @@ -382,8 +369,8 @@ This is how you create a custom root folder inside the zip file. - When updating a plugin, WordPress expects a folder in the root of the zip file which matches the plugin name. So be aware that this may affect the plugin update process. - `--root-folder` - Add a custom root folder to the zip file. -- `npm run plugin-zip` - By default, unzipping your plugin's zip file will result in a folder with the same name as your plugin. -- `npm run plugin-zip --root-folder='custom-directory'` - Your plugin's zip file will be unzipped into a folder named `custom-directory`. +- `npm run plugin-zip` - By default, unzipping your plugin’s zip file will result in a folder with the same name as your plugin. +- `npm run plugin-zip --root-folder='custom-directory'` - Your plugin’s zip file will be unzipped into a folder named `custom-directory`. - `npm run plugin-zip --no-root-folder` - This will create a zip file that has no folder inside, your plugin files will be unzipped directly into the target directory. ### `start` @@ -392,19 +379,7 @@ Transforms your code according the configuration provided so it’s ready for de _For single builds, better suited for production, see the [build](#build) script._ -The entry points for your project get detected by scanning all script fields in `block.json` files located in the `src` directory. The script fields in `block.json` should pass relative paths to `block.json` in the same folder. - -_Example:_ - -```json -{ - "editorScript": "file:index.js", - "script": "file:script.js", - "viewScript": "file:view.js" -} -``` - -The fallback entry point is `src/index.js` (other supported extensions: `.jsx`, `.ts`, and `.tsx`) in case there is no `block.json` file found. In that scenario, the output generated will be written to `build/index.js`. +#### Usage _Example:_ @@ -415,7 +390,7 @@ _Example:_ "start:hot": "wp-scripts start --hot", "start:custom": "wp-scripts start entry-one.js entry-two.js --output-path=custom", "start:copy-php": "wp-scripts start --webpack-copy-php", - "start:custom-directory": "wp-scripts start --webpack-src-dir=custom-directory" + "start:custom-directory": "wp-scripts start --source-path=custom-directory" } } ``` @@ -435,15 +410,17 @@ This script automatically use the optimized config but sometimes you may want to - `--webpack-bundle-analyzer` – enables visualization for the size of webpack output files with an interactive zoomable treemap. - `--webpack-copy-php` – enables copying all PHP files from the source directory ( default is `src` ) and its subfolders to the output directory. - `--webpack-devtool` – controls how source maps are generated. See options at https://webpack.js.org/configuration/devtool/#devtool. -- `--webpack-no-externals` – disables scripts' assets generation, and omits the list of default externals. -- `--webpack-src-dir` – Allows customization of the source code directory. Default is `src`. -- `--output-path` – Allows customization of the output directory. Default is `build`. +- `--webpack-no-externals` – disables scripts’ assets generation, and omits the list of default externals. +- `--source-path` – allows customization of the source directory. The default is the project root `.` when [entry points are listed](#listing-entry-points) in the command, or `src` otherwise. +- `--output-path` – allows customization of the output directory. The default is the `build` folder. Experimental support for the block.json `viewScriptModule` field is available via the `--experimental-modules` option. With this option enabled, script and module fields will all be compiled. The `viewScriptModule` field is analogous to the `viewScript` field, but will compile a module and should be registered in WordPress using the Modules API. +Learn more about [using build scripts](#using-build-scripts) to optimize the development experience based on your specific needs. + #### 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. @@ -492,7 +469,7 @@ We enforce that all tests run serially in the current process using [--runInBand When tests fail, both a screenshot and an HTML snapshot will be taken of the page and stored in the `artifacts/` directory at the root of your project. These snapshots may help debug failed tests during development or when running tests in a CI environment. -The `artifacts/` directory can be customized by setting the `WP_ARTIFACTS_PATH` environment variable to the relative path of the desired directory within your project's root. For example: to change the default directory from `artifacts/` to `my/custom/artifacts`, you could use `WP_ARTIFACTS_PATH=my/custom/artifacts npm run test:e2e`. +The `artifacts/` directory can be customized by setting the `WP_ARTIFACTS_PATH` environment variable to the relative path of the desired directory within your project’s root. For example: to change the default directory from `artifacts/` to `my/custom/artifacts`, you could use `WP_ARTIFACTS_PATH=my/custom/artifacts npm run test:e2e`. #### Advanced information @@ -581,11 +558,11 @@ To do so, you can add a file called `playwright.config.ts` or `playwright.config When tests fail, snapshots will be taken of the page and stored in the `artifacts/` directory at the root of your project. These snapshots may help debug failed tests during development or when running tests in a CI environment. -The `artifacts/` directory can be customized by setting the `WP_ARTIFACTS_PATH` environment variable to the relative path of the desired directory within your project's root. For example: to change the default directory from `artifacts/` to `my/custom/artifacts`, you could use `WP_ARTIFACTS_PATH=my/custom/artifacts npm run test:playwright`. +The `artifacts/` directory can be customized by setting the `WP_ARTIFACTS_PATH` environment variable to the relative path of the desired directory within your project’s root. For example: to change the default directory from `artifacts/` to `my/custom/artifacts`, you could use `WP_ARTIFACTS_PATH=my/custom/artifacts npm run test:playwright`. #### Advanced information -You are able to use all of Playwright's [CLI options](https://playwright.dev/docs/test-cli#reference). You can also run `./node_modules/.bin/wp-scripts test-playwright --help` or `npm run test:playwright:help` (as mentioned above) to view all the available options. Learn more in the [Advanced Usage](#advanced-usage) section. +You are able to use all of Playwright’s [CLI options](https://playwright.dev/docs/test-cli#reference). You can also run `./node_modules/.bin/wp-scripts test-playwright --help` or `npm run test:playwright:help` (as mentioned above) to view all the available options. Learn more in the [Advanced Usage](#advanced-usage) section. ## Passing Node.js options @@ -639,30 +616,49 @@ To also debug the browser context, run `wp-scripts --inspect-brk test-e2e --pupp For more e2e debugging tips check out the [Puppeteer debugging docs](https://developers.google.com/web/tools/puppeteer/debugging). -## Advanced Usage +## Using build scripts -In general, this package should be used with the set of recommended config files. While it’s possible to override every single config file provided, if you have to do it, it means that your use case is far more complicated than anticipated. If that happens, it would be better to avoid using the whole abstraction layer and set up your project with full control over tooling used. +The `build` and `start` commands use [webpack](https://webpack.js.org/) behind the scenes. webpack is used to bundle and optimize code for web applications, enabling developers to manage dependencies efficiently, enhance performance, and simplify the development workflow. -### Working with build scripts +### Listing entry points -The `build` and `start` commands use [webpack](https://webpack.js.org/) behind the scenes. webpack is a tool that helps you transform your code into something else. For example: it can take code written in ESNext and output ES5 compatible code that is minified for production. +The simplest way to list JavaScript entry points is to pass them as arguments for the command. -#### Default webpack config +_Example:_ -`@wordpress/scripts` bundles the default webpack config used as a base by the WordPress editor. These are the defaults: +```bash +wp-scripts build entry-one.js entry-two.js +``` -- [Entry](https://webpack.js.org/configuration/entry-context/#entry): the entry points for your project get detected by scanning all script fields in `block.json` files located in the `src` directory. The fallback entry point is `src/index.js` (other supported extensions: `.jsx`, `.ts`, and `.tsx`) in case there is no `block.json` file found. -- [Output](https://webpack.js.org/configuration/output): `build/[name].js`, for example: `build/index.js`, or `build/my-block/index.js`. -- [Loaders](https://webpack.js.org/loaders/): - - [`babel-loader`](https://webpack.js.org/loaders/babel-loader/) allows transpiling JavaScript and TypeScript files using Babel and webpack. - - [`@svgr/webpack`](https://www.npmjs.com/package/@svgr/webpack) and [`url-loader`](https://webpack.js.org/loaders/url-loader/) makes it possible to handle SVG files in JavaScript code. - - [`css-loader`](https://webpack.js.org/loaders/css-loader/) chained with [`postcss-loader`](https://webpack.js.org/loaders/postcss-loader/) and [sass-loader](https://webpack.js.org/loaders/sass-loader/) let webpack process CSS, SASS or SCSS files referenced in JavaScript files. -- [Plugins](https://webpack.js.org/configuration/plugins) (among others): - - [`CopyWebpackPlugin`](https://webpack.js.org/plugins/copy-webpack-plugin/) copies all `block.json` files discovered in the `src` directory to the build directory. - - [`MiniCssExtractPlugin`](https://webpack.js.org/plugins/mini-css-extract-plugin/) extracts CSS into separate files. It creates a CSS file per JavaScript entry point which contains CSS. - - [`@wordpress/dependency-extraction-webpack-plugin`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/dependency-extraction-webpack-plugin/README.md) is used with the default configuration to ensure that WordPress provided scripts are not included in the built bundle. +The default location for the source files is the project’s root. In effect, the command above will look for `entry-one.js` and `entry-two.js` in the project’s root and output the generated files into the `build` directory. + +### Automatic block.json detection and the source code directory -#### Using CSS +A convenient alternative for blocks is using automatic entry point detection. In that case, the source code directory (the default is `./src`) and its subdirectories are scanned for the existence of `block.json` files. If one or more are found, the JavaScript files listed in metadata are treated as entry points and will be output into corresponding folders in the `build` directory. The script fields in `block.json` should pass relative paths to `block.json` in the same folder. + +_Example:_ + +```json +{ + "editorScript": "file:index.js", + "script": "file:script.js", + "viewScript": "file:view.js" +} +``` + +This allows for the creation of multiple blocks that use a single build process triggered with a simple command: + +```bash +wp-scripts build +``` + +The source directory can be customized using the `--source-path` flag and the output directory with the `--output-path` flag. + +### Fallback entry point + +The fallback entry point is `src/index.js` (other supported extensions: `.jsx`, `.ts`, and `.tsx`) in case there is no `block.json` file found. In that scenario, the output generated will be written to `build/index.js`. + +### Importing styles in JavaScript _Example:_ @@ -694,19 +690,19 @@ When you run the build using the default command `wp-scripts build` (also applie 1. `index.css` – all imported CSS files are bundled into one chunk named after the entry point, which defaults to `index.js`, and thus the file created becomes `index.css`. This is for styles used only in the editor. 2. `style-index.css` – imported `style.css` file(s) (applies to PCSS, SASS and SCSS extensions) get bundled into one `style-index.css` file that is meant to be used both on the front-end and in the editor. -You can also have multiple entry points as described in the docs for the script: +For example, when the project has two entry points: ```bash -wp-scripts start entry-one.js entry-two.js --output-path=custom +wp-scripts build entry-one.js entry-two.js ``` -If you do so, then CSS files generated will follow the names of the entry points: `entry-one.css` and `entry-two.css`. +In that case, the CSS generated based on import statements in the JavaScript code will follow the names of the entry points: `entry-one.css` and `entry-two.css`. -Avoid using `style` keyword in an entry point name, this might break your build process. +_Important:_ Avoid using `style` keyword in an entry point name, this might break your build process. You can also bundle CSS modules by prefixing `.module` to the extension, e.g. `style.module.scss`. Otherwise, these files are handled like all other `style.scss`. They will also be extracted into `style-index.css`. -#### Using fonts and images +### Using fonts and images It is possible to reference font (`woff`, `woff2`, `eot`, `ttf` and `otf`) and image (`bmp`, `png`, `jpg`, `jpeg`, `gif` and `wepb`) files from CSS that is controlled by webpack as explained in the previous section. @@ -724,7 +720,7 @@ _Example:_ } ``` -#### Using SVG +### Using SVG _Example:_ @@ -739,18 +735,37 @@ const App = () => ( ); ``` -#### Provide your own webpack config +## Advanced Usage + +This package should generally be used with the set of recommended config files. While it’s possible to override every config file provided, if you have to do it, your use case is far more complicated than anticipated. If that happens, it would be better to avoid using the whole abstraction layer and set up your project with full control over the tooling used. + +### Default webpack config + +`@wordpress/scripts` bundles the default webpack config used as a base by the WordPress editor. These are the defaults: + +- [Entry](https://webpack.js.org/configuration/entry-context/#entry): the entry points for your project get detected by scanning all script fields in `block.json` files located in the `src` directory. The fallback entry point is `src/index.js` (other supported extensions: `.jsx`, `.ts`, and `.tsx`) in case there is no `block.json` file found. +- [Output](https://webpack.js.org/configuration/output): `build/[name].js`, for example: `build/index.js`, or `build/my-block/index.js`. +- [Loaders](https://webpack.js.org/loaders/): + - [`babel-loader`](https://webpack.js.org/loaders/babel-loader/) allows transpiling JavaScript and TypeScript files using Babel and webpack. + - [`@svgr/webpack`](https://www.npmjs.com/package/@svgr/webpack) and [`url-loader`](https://webpack.js.org/loaders/url-loader/) makes it possible to handle SVG files in JavaScript code. + - [`css-loader`](https://webpack.js.org/loaders/css-loader/) chained with [`postcss-loader`](https://webpack.js.org/loaders/postcss-loader/) and [sass-loader](https://webpack.js.org/loaders/sass-loader/) let webpack process CSS, SASS or SCSS files referenced in JavaScript files. +- [Plugins](https://webpack.js.org/configuration/plugins) (among others): + - [`CopyWebpackPlugin`](https://webpack.js.org/plugins/copy-webpack-plugin/) copies all `block.json` files discovered in the `src` directory to the build directory. + - [`MiniCssExtractPlugin`](https://webpack.js.org/plugins/mini-css-extract-plugin/) extracts CSS into separate files. It creates a CSS file per JavaScript entry point which contains CSS. + - [`@wordpress/dependency-extraction-webpack-plugin`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/dependency-extraction-webpack-plugin/README.md) is used with the default configuration to ensure that WordPress provided scripts are not included in the built bundle. + +### Provide your own webpack config Should there be any situation where you want to provide your own webpack config, you can do so. The `build` and `start` commands will use your provided file when: - the command receives a `--config` argument. Example: `wp-scripts build --config my-own-webpack-config.js`. - there is a file called `webpack.config.js` or `webpack.config.babel.js` in the top-level directory of your project (at the same level as `package.json`). -##### Extending the webpack config +#### Extending the webpack config To extend the provided webpack config, or replace subsections within the provided webpack config, you can provide your own `webpack.config.js` file, `require` the provided `webpack.config.js` file, and use the [`spread` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) to import all of or part of the provided configuration. -In the example below, a `webpack.config.js` file is added to the root folder extending the provided webpack config to include custom logic to parse module's source and convert it to a JavaScript object using [`toml`](https://www.npmjs.com/package/toml). It may be useful to import toml or other non-JSON files as JSON, without specific loaders: +In the example below, a `webpack.config.js` file is added to the root folder extending the provided webpack config to include custom logic to parse module’s source and convert it to a JavaScript object using [`toml`](https://www.npmjs.com/package/toml). It may be useful to import toml or other non-JSON files as JSON, without specific loaders: ```javascript const toml = require( 'toml' ); @@ -781,8 +796,8 @@ If you follow this approach, please, be aware that: ## 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. +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. -To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). +To find out more about contributing to this package or Gutenberg as a whole, please read the project’s main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). <br /><br /><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p> diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js index 1829da5cdc15da..f0e425a8d5998f 100644 --- a/packages/scripts/config/webpack.config.js +++ b/packages/scripts/config/webpack.config.js @@ -9,7 +9,6 @@ const browserslist = require( 'browserslist' ); const MiniCSSExtractPlugin = require( 'mini-css-extract-plugin' ); const { basename, dirname, relative, resolve, sep } = require( 'path' ); const ReactRefreshWebpackPlugin = require( '@pmmmwh/react-refresh-webpack-plugin' ); -const RtlCssPlugin = require( 'rtlcss-webpack-plugin' ); const TerserPlugin = require( 'terser-webpack-plugin' ); const { realpathSync } = require( 'fs' ); const { sync: glob } = require( 'fast-glob' ); @@ -23,19 +22,20 @@ const postcssPlugins = require( '@wordpress/postcss-plugins-preset' ); /** * Internal dependencies */ +const PhpFilePathsPlugin = require( '../plugins/php-file-paths-plugin' ); +const RtlCssPlugin = require( '../plugins/rtlcss-webpack-plugin' ); const { fromConfigRoot, hasBabelConfig, hasArgInCLI, hasCssnanoConfig, hasPostCSSConfig, - getWordPressSrcDirectory, + getProjectSourcePath, getWebpackEntryPoints, getAsBooleanFromENV, getBlockJsonModuleFields, getBlockJsonScriptFields, fromProjectRoot, - PhpFilePathsPlugin, } = require( '../utils' ); const isProduction = process.env.NODE_ENV === 'production'; @@ -302,14 +302,14 @@ const scriptConfig = { } ), new PhpFilePathsPlugin( { - context: getWordPressSrcDirectory(), + context: getProjectSourcePath(), props: [ 'render', 'variations' ], } ), new CopyWebpackPlugin( { patterns: [ { from: '**/block.json', - context: getWordPressSrcDirectory(), + context: getProjectSourcePath(), noErrorOnMissing: true, transform( content, absoluteFrom ) { const convertExtension = ( path ) => { @@ -346,7 +346,7 @@ const scriptConfig = { const runtimePath = relative( dirname( absoluteFrom ), fromProjectRoot( - getWordPressSrcDirectory() + + getProjectSourcePath() + sep + 'runtime.js' ) @@ -375,7 +375,7 @@ const scriptConfig = { }, { from: '**/*.php', - context: getWordPressSrcDirectory(), + context: getProjectSourcePath(), noErrorOnMissing: true, filter: ( filepath ) => { return ( @@ -396,9 +396,7 @@ const scriptConfig = { filename: '[name].css', } ), // RtlCssPlugin to generate RTL CSS files. - new RtlCssPlugin( { - filename: `[name]-rtl.css`, - } ), + new RtlCssPlugin(), // React Fast Refresh. hasReactFastRefresh && new ReactRefreshWebpackPlugin(), // WP_NO_EXTERNALS global variable controls whether scripts' assets get @@ -417,7 +415,7 @@ if ( hasExperimentalModulesFlag ) { /** @type {ReadonlyArray<string>} */ this.blockJsonFiles = glob( '**/block.json', { absolute: true, - cwd: fromProjectRoot( getWordPressSrcDirectory() ), + cwd: fromProjectRoot( getProjectSourcePath() ), } ); } diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 7b0d37a5344b25..e6f9cf39335a78 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -80,8 +80,8 @@ "react-refresh": "^0.14.0", "read-pkg-up": "^7.0.1", "resolve-bin": "^0.4.0", - "rtlcss-webpack-plugin": "^4.0.7", - "sass": "^1.50.1", + "rtlcss": "^4.3.0", + "sass": "^1.54.0", "sass-loader": "^16.0.3", "schema-utils": "^4.2.0", "source-map-loader": "^3.0.0", diff --git a/packages/scripts/utils/php-file-paths-plugin.js b/packages/scripts/plugins/php-file-paths-plugin/index.js similarity index 92% rename from packages/scripts/utils/php-file-paths-plugin.js rename to packages/scripts/plugins/php-file-paths-plugin/index.js index 6f95dae6505a80..df39e1626a8766 100644 --- a/packages/scripts/utils/php-file-paths-plugin.js +++ b/packages/scripts/plugins/php-file-paths-plugin/index.js @@ -6,7 +6,7 @@ const { validate } = require( 'schema-utils' ); /** * Internal dependencies */ -const { getPhpFilePaths } = require( './config' ); +const { getPhpFilePaths } = require( '../../utils' ); const phpFilePathsPluginSchema = { type: 'object', @@ -57,4 +57,4 @@ class PhpFilePathsPlugin { } } -module.exports = { PhpFilePathsPlugin }; +module.exports = PhpFilePathsPlugin; diff --git a/packages/scripts/plugins/rtlcss-webpack-plugin/index.js b/packages/scripts/plugins/rtlcss-webpack-plugin/index.js new file mode 100644 index 00000000000000..c46c01320c763e --- /dev/null +++ b/packages/scripts/plugins/rtlcss-webpack-plugin/index.js @@ -0,0 +1,66 @@ +/** + * Parts of this source were derived and modified from the package + * rtlcss-webpack-plugin, released under the MIT license. + * + * https://github.com/wix-incubator/rtlcss-webpack-plugin + * + * Copyright (c) 2018 Wix.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/** + * External dependencies + */ +const path = require( 'node:path' ); +const rtlcss = require( 'rtlcss' ); +const webpack = require( 'webpack' ); + +const cssOnly = ( filename ) => path.extname( filename ) === '.css'; + +class RtlCssPlugin { + processAssets = ( compilation, callback ) => { + const chunks = Array.from( compilation.chunks ); + + // Explore each chunk (build output): + chunks.forEach( ( chunk ) => { + // Explore each asset filename generated by the chunk: + const files = Array.from( chunk.files ); + + files.filter( cssOnly ).forEach( ( filename ) => { + // Get the asset source for each file generated by the chunk: + const src = compilation.assets[ filename ].source(); + const dst = rtlcss.process( src ); + const dstFileName = compilation.getPath( '[name]-rtl.css', { + chunk, + cssFileName: filename, + } ); + + compilation.assets[ dstFileName ] = + new webpack.sources.RawSource( dst ); + chunk.files.add( dstFileName ); + } ); + } ); + + callback(); + }; + + apply( compiler ) { + compiler.hooks.compilation.tap( 'RtlCssPlugin', ( compilation ) => { + compilation.hooks.processAssets.tapAsync( + { + name: 'TPAStylePlugin.pluginName', + stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE, + }, + ( chunks, callback ) => + this.processAssets( compilation, callback ) + ); + } ); + } +} + +module.exports = RtlCssPlugin; diff --git a/packages/scripts/scripts/build.js b/packages/scripts/scripts/build.js index 0eef2afb451bfc..8c7b768ba3e694 100644 --- a/packages/scripts/scripts/build.js +++ b/packages/scripts/scripts/build.js @@ -7,31 +7,11 @@ const { sync: resolveBin } = require( 'resolve-bin' ); /** * Internal dependencies */ -const { getWebpackArgs, hasArgInCLI, getArgFromCLI } = require( '../utils' ); +const { getWebpackArgs } = require( '../utils' ); 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; -} - -if ( hasArgInCLI( '--webpack-bundle-analyzer' ) ) { - process.env.WP_BUNDLE_ANALYZER = true; -} - -if ( hasArgInCLI( '--webpack-copy-php' ) ) { - process.env.WP_COPY_PHP_FILES_TO_DIST = true; -} - -process.env.WP_SRC_DIRECTORY = hasArgInCLI( '--webpack-src-dir' ) - ? getArgFromCLI( '--webpack-src-dir' ) - : 'src'; - const { status } = spawn( resolveBin( 'webpack' ), getWebpackArgs(), { stdio: 'inherit', } ); diff --git a/packages/scripts/scripts/start.js b/packages/scripts/scripts/start.js index 6296192ef302b1..fd0a191f168e9e 100644 --- a/packages/scripts/scripts/start.js +++ b/packages/scripts/scripts/start.js @@ -7,33 +7,9 @@ const { sync: resolveBin } = require( 'resolve-bin' ); /** * Internal dependencies */ -const { getArgFromCLI, getWebpackArgs, hasArgInCLI } = require( '../utils' ); +const { 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; -} - -if ( hasArgInCLI( '--webpack-bundle-analyzer' ) ) { - process.env.WP_BUNDLE_ANALYZER = true; -} - -if ( hasArgInCLI( '--webpack--devtool' ) ) { - process.env.WP_DEVTOOL = getArgFromCLI( '--webpack--devtool' ); -} - -if ( hasArgInCLI( '--webpack-copy-php' ) ) { - process.env.WP_COPY_PHP_FILES_TO_DIST = true; -} - -process.env.WP_SRC_DIRECTORY = hasArgInCLI( '--webpack-src-dir' ) - ? getArgFromCLI( '--webpack-src-dir' ) - : 'src'; - const webpackArgs = getWebpackArgs(); if ( hasArgInCLI( '--hot' ) ) { webpackArgs.unshift( 'serve' ); diff --git a/packages/scripts/utils/config.js b/packages/scripts/utils/config.js index 3d99f3784859df..be6f1831378911 100644 --- a/packages/scripts/utils/config.js +++ b/packages/scripts/utils/config.js @@ -9,6 +9,7 @@ const { sync: glob } = require( 'fast-glob' ); * Internal dependencies */ const { + getArgFromCLI, getArgsFromCLI, getFileArgsFromCLI, hasArgInCLI, @@ -114,9 +115,37 @@ const getWebpackArgs = () => { // Gets all args from CLI without those prefixed with `--webpack`. let webpackArgs = getArgsFromCLI( [ '--experimental-modules', + '--source-path', '--webpack', ] ); + if ( hasArgInCLI( '--experimental-modules' ) ) { + process.env.WP_EXPERIMENTAL_MODULES = true; + } + + if ( hasArgInCLI( '--source-path' ) ) { + process.env.WP_SOURCE_PATH = getArgFromCLI( '--source-path' ); + } else if ( hasArgInCLI( '--webpack-src-dir' ) ) { + // Backwards compatibility. + process.env.WP_SOURCE_PATH = getArgFromCLI( '--webpack-src-dir' ); + } + + if ( hasArgInCLI( '--webpack-bundle-analyzer' ) ) { + process.env.WP_BUNDLE_ANALYZER = true; + } + + if ( hasArgInCLI( '--webpack-copy-php' ) ) { + process.env.WP_COPY_PHP_FILES_TO_DIST = true; + } + + if ( hasArgInCLI( '--webpack--devtool' ) ) { + process.env.WP_DEVTOOL = getArgFromCLI( '--webpack--devtool' ); + } + + if ( hasArgInCLI( '--webpack-no-externals' ) ) { + process.env.WP_NO_EXTERNALS = true; + } + const hasWebpackOutputOption = hasArgInCLI( '-o' ) || hasArgInCLI( '--output' ); if ( @@ -136,10 +165,6 @@ const getWebpackArgs = () => { const pathToEntry = ( path ) => { const entryName = basename( path, '.js' ); - if ( ! path.startsWith( './' ) ) { - path = './' + path; - } - return [ entryName, path ]; }; @@ -162,7 +187,11 @@ const getWebpackArgs = () => { const [ entryName, path ] = fileArg.includes( '=' ) ? fileArg.split( '=' ) : pathToEntry( fileArg ); - entry[ entryName ] = path; + entry[ entryName ] = fromProjectRoot( + process.env.WP_SOURCE_PATH + ? join( process.env.WP_SOURCE_PATH, path ) + : path + ); } ); process.env.WP_ENTRY = JSON.stringify( entry ); } @@ -176,20 +205,20 @@ const getWebpackArgs = () => { }; /** - * Returns the WordPress source directory. It defaults to 'src' if the - * `process.env.WP_SRC_DIRECTORY` variable is not set. + * Returns the project source path. It defaults to 'src' if the + * `process.env.WP_SOURCE_PATH` variable is not set. * * @return {string} The WordPress source directory. */ -function getWordPressSrcDirectory() { - return process.env.WP_SRC_DIRECTORY || 'src'; +function getProjectSourcePath() { + return process.env.WP_SOURCE_PATH || 'src'; } /** - * Detects the list of entry points to use with webpack. There are three ways to do this: - * 1. Use the legacy webpack 4 format passed as CLI arguments. - * 2. Scan `block.json` files for scripts. - * 3. Fallback to `src/index.*` file. + * Detects the list of entry points to use with webpack. There are three alternative ways to do this: + * 1. Use the recommended command format that lists the paths to JavaScript files. + * 2. Scan `block.json` files to detect referenced JavaScript and PHP files automatically. + * 3. Fallback to the `src/index.*` file. * * @see https://webpack.js.org/concepts/entry-points/ * @@ -200,31 +229,32 @@ 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`. + // 1. Uses the recommended command format that lists entry points as paths to JavaScript files. + // Example: `wp-scripts build one.js two.js`. if ( process.env.WP_ENTRY ) { return buildType === 'script' ? JSON.parse( process.env.WP_ENTRY ) : {}; } - // Continue only if the source directory exists. - if ( ! hasProjectFile( getWordPressSrcDirectory() ) ) { + // Continues only if the source directory exists. Defaults to "src" if not explicitly set in the command. + if ( ! hasProjectFile( getProjectSourcePath() ) ) { warn( - `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.` + `Source directory "${ getProjectSourcePath() }" was not found. Please confirm there is a "src" directory in the root or the value passed with "--output-path" 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. + // It scans all discovered files, looks for JavaScript assets, and converts them to entry points. const blockMetadataFiles = glob( '**/block.json', { absolute: true, - cwd: fromProjectRoot( getWordPressSrcDirectory() ), + cwd: fromProjectRoot( getProjectSourcePath() ), } ); if ( blockMetadataFiles.length > 0 ) { const srcDirectory = fromProjectRoot( - getWordPressSrcDirectory() + sep + getProjectSourcePath() + sep ); const entryPoints = {}; @@ -276,7 +306,7 @@ function getWebpackEntryPoints( buildType ) { ) }" listed in "${ blockMetadataFile.replace( fromProjectRoot( sep ), '' - ) }". File is located outside of the "${ getWordPressSrcDirectory() }" directory.` + ) }". File is located outside of the "${ getProjectSourcePath() }" directory.` ); continue; } @@ -290,7 +320,7 @@ function getWebpackEntryPoints( buildType ) { `${ entryName }.?(m)[jt]s?(x)`, { absolute: true, - cwd: fromProjectRoot( getWordPressSrcDirectory() ), + cwd: fromProjectRoot( getProjectSourcePath() ), } ); @@ -302,7 +332,7 @@ function getWebpackEntryPoints( buildType ) { ) }" listed in "${ blockMetadataFile.replace( fromProjectRoot( sep ), '' - ) }". File does not exist in the "${ getWordPressSrcDirectory() }" directory.` + ) }". File does not exist in the "${ getProjectSourcePath() }" directory.` ); continue; } @@ -322,15 +352,15 @@ function getWebpackEntryPoints( buildType ) { } // 3. Checks whether a standard file name can be detected in the defined source directory, - // and converts the discovered file to entry point. + // and converts the discovered file to entry point. const [ entryFile ] = glob( 'index.[jt]s?(x)', { absolute: true, - cwd: fromProjectRoot( getWordPressSrcDirectory() ), + cwd: fromProjectRoot( getProjectSourcePath() ), } ); if ( ! entryFile ) { warn( - `No entry file discovered in the "${ getWordPressSrcDirectory() }" directory.` + `No entry file discovered in the "${ getProjectSourcePath() }" directory.` ); return {}; } @@ -412,10 +442,10 @@ function getPhpFilePaths( context, props ) { module.exports = { getJestOverrideConfigFile, + getPhpFilePaths, + getProjectSourcePath, getWebpackArgs, - getWordPressSrcDirectory, getWebpackEntryPoints, - getPhpFilePaths, hasBabelConfig, hasCssnanoConfig, hasJestConfig, diff --git a/packages/scripts/utils/index.js b/packages/scripts/utils/index.js index cb7e592f83d554..b26df4bd479d9a 100644 --- a/packages/scripts/utils/index.js +++ b/packages/scripts/utils/index.js @@ -13,10 +13,10 @@ const { } = require( './cli' ); const { getJestOverrideConfigFile, + getPhpFilePaths, + getProjectSourcePath, getWebpackArgs, - getWordPressSrcDirectory, getWebpackEntryPoints, - getPhpFilePaths, hasBabelConfig, hasCssnanoConfig, hasJestConfig, @@ -29,7 +29,6 @@ const { getBlockJsonModuleFields, getBlockJsonScriptFields, } = require( './block-json' ); -const { PhpFilePathsPlugin } = require( './php-file-paths-plugin' ); module.exports = { fromProjectRoot, @@ -41,10 +40,10 @@ module.exports = { getJestOverrideConfigFile, getNodeArgsFromCLI, getPackageProp, + getPhpFilePaths, + getProjectSourcePath, getWebpackArgs, - getWordPressSrcDirectory, getWebpackEntryPoints, - getPhpFilePaths, getBlockJsonModuleFields, getBlockJsonScriptFields, hasArgInCLI, @@ -56,6 +55,5 @@ module.exports = { hasPostCSSConfig, hasPrettierConfig, hasProjectFile, - PhpFilePathsPlugin, spawnScript, }; diff --git a/packages/server-side-render/README.md b/packages/server-side-render/README.md index ef7cd9bf0189c1..ba6fae302ca0a8 100644 --- a/packages/server-side-render/README.md +++ b/packages/server-side-render/README.md @@ -79,7 +79,7 @@ add_filter( 'rest_endpoints', 'add_rest_method'); ### skipBlockSupportAttributes -Remove attributes and style properties applied by the block supports. This prevents duplication of styles in the block wrapper and the `ServerSideRender` components. Even if certain features skip serialization to HTML markup by `skipSerialization`, all attributes and style properties are removed. +Remove attributes and style properties applied by the block supports. This prevents duplication of styles in the block wrapper and the `ServerSideRender` components. Even if certain features skip serialization to HTML markup by `__experimentalSkipSerialization`, all attributes and style properties are removed. - Type: `Boolean` - Required: No diff --git a/packages/shortcode/tsconfig.json b/packages/shortcode/tsconfig.json index 79aa09d0ad56e3..2ab16a25d51788 100644 --- a/packages/shortcode/tsconfig.json +++ b/packages/shortcode/tsconfig.json @@ -2,10 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "checkJs": false - }, - "references": [], - "include": [ "src" ] + } } diff --git a/packages/style-engine/tsconfig.json b/packages/style-engine/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/style-engine/tsconfig.json +++ b/packages/style-engine/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/sync/tsconfig.json b/packages/sync/tsconfig.json index 40b3ecb72f9aba..f0a5cb0530d297 100644 --- a/packages/sync/tsconfig.json +++ b/packages/sync/tsconfig.json @@ -2,10 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "node" ] }, - "include": [ "src/**/*" ], "references": [ { "path": "../url" } ] } diff --git a/packages/token-list/tsconfig.json b/packages/token-list/tsconfig.json index d1947d4c52ffdf..7ff060ab6ce105 100644 --- a/packages/token-list/tsconfig.json +++ b/packages/token-list/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/undo-manager/tsconfig.json b/packages/undo-manager/tsconfig.json index 055c19d5bf513d..a3c336bec45609 100644 --- a/packages/undo-manager/tsconfig.json +++ b/packages/undo-manager/tsconfig.json @@ -2,10 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "node" ] }, - "references": [ { "path": "../is-shallow-equal" } ], - "include": [ "src/**/*" ] + "references": [ { "path": "../is-shallow-equal" } ] } diff --git a/packages/upload-media/CHANGELOG.md b/packages/upload-media/CHANGELOG.md new file mode 100644 index 00000000000000..e04ce921cdfdc4 --- /dev/null +++ b/packages/upload-media/CHANGELOG.md @@ -0,0 +1,5 @@ +<!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. --> + +## Unreleased + +Initial release. diff --git a/packages/upload-media/README.md b/packages/upload-media/README.md new file mode 100644 index 00000000000000..982e59148fe87c --- /dev/null +++ b/packages/upload-media/README.md @@ -0,0 +1,136 @@ +# (Experimental) Upload Media + +This module is a media upload handler with a queue-like system that is implemented using a custom `@wordpress/data` store. + +Such a system is useful for additional client-side processing of media files (e.g. image compression) before uploading them to a server. + +It is typically used by `@wordpress/block-editor` but can also be leveraged outside of it. + +## Installation + +Install the module + +```bash +npm install @wordpress/upload-media --save +``` + +## Usage + +This is a basic example of how one can interact with the upload data store: + +```js +import { store as uploadStore } from '@wordpress/upload-media'; +import { dispatch } from '@wordpress/data'; + +dispatch( uploadStore ).updateSettings( /* ... */ ); +dispatch( uploadStore ).addItems( [ + /* ... */ +] ); +``` + +Refer to the API reference below or the TypeScript types for further help. + +## API Reference + +### Actions + +The following set of dispatching action creators are available on the object returned by `wp.data.dispatch( 'core/upload-media' )`: + +<!-- START TOKEN(Autogenerated actions|src/store/actions.ts) --> + +#### addItems + +Adds a new item to the upload queue. + +_Parameters_ + +- _$0_ `AddItemsArgs`: +- _$0.files_ `AddItemsArgs[ 'files' ]`: Files +- _$0.onChange_ `[AddItemsArgs[ 'onChange' ]]`: Function called each time a file or a temporary representation of the file is available. +- _$0.onSuccess_ `[AddItemsArgs[ 'onSuccess' ]]`: Function called after the file is uploaded. +- _$0.onBatchSuccess_ `[AddItemsArgs[ 'onBatchSuccess' ]]`: Function called after a batch of files is uploaded. +- _$0.onError_ `[AddItemsArgs[ 'onError' ]]`: Function called when an error happens. +- _$0.additionalData_ `[AddItemsArgs[ 'additionalData' ]]`: Additional data to include in the request. +- _$0.allowedTypes_ `[AddItemsArgs[ 'allowedTypes' ]]`: Array with the types of media that can be uploaded, if unset all types are allowed. + +#### cancelItem + +Cancels an item in the queue based on an error. + +_Parameters_ + +- _id_ `QueueItemId`: Item ID. +- _error_ `Error`: Error instance. +- _silent_ Whether to cancel the item silently, without invoking its `onError` callback. + +<!-- END TOKEN(Autogenerated actions|src/store/actions.ts) --> + +### Selectors + +The following selectors are available on the object returned by `wp.data.select( 'core/upload-media' )`: + +<!-- START TOKEN(Autogenerated selectors|src/store/selectors.ts) --> + +#### getItems + +Returns all items currently being uploaded. + +_Parameters_ + +- _state_ `State`: Upload state. + +_Returns_ + +- `QueueItem[]`: Queue items. + +#### getSettings + +Returns the media upload settings. + +_Parameters_ + +- _state_ `State`: Upload state. + +_Returns_ + +- `Settings`: Settings + +#### isUploading + +Determines whether any upload is currently in progress. + +_Parameters_ + +- _state_ `State`: Upload state. + +_Returns_ + +- `boolean`: Whether any upload is currently in progress. + +#### isUploadingById + +Determines whether an upload is currently in progress given an attachment ID. + +_Parameters_ + +- _state_ `State`: Upload state. +- _attachmentId_ `number`: Attachment ID. + +_Returns_ + +- `boolean`: Whether upload is currently in progress for the given attachment. + +#### isUploadingByUrl + +Determines whether an upload is currently in progress given an attachment URL. + +_Parameters_ + +- _state_ `State`: Upload state. +- _url_ `string`: Attachment URL. + +_Returns_ + +- `boolean`: Whether upload is currently in progress for the given attachment. + +<!-- END TOKEN(Autogenerated selectors|src/store/selectors.ts) --> diff --git a/packages/upload-media/package.json b/packages/upload-media/package.json new file mode 100644 index 00000000000000..14ae4f77dc5cb9 --- /dev/null +++ b/packages/upload-media/package.json @@ -0,0 +1,45 @@ +{ + "name": "@wordpress/upload-media", + "version": "1.0.0-prerelease", + "private": true, + "description": "Core media upload logic.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "media" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/upload-media/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/upload-media" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "wpScript": true, + "types": "build-types", + "dependencies": { + "@shopify/web-worker": "^6.4.0", + "@wordpress/api-fetch": "file:../api-fetch", + "@wordpress/blob": "file:../blob", + "@wordpress/compose": "file:../compose", + "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/preferences": "file:../preferences", + "@wordpress/private-apis": "file:../private-apis", + "@wordpress/url": "file:../url", + "uuid": "^9.0.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/upload-media/src/components/provider/index.tsx b/packages/upload-media/src/components/provider/index.tsx new file mode 100644 index 00000000000000..0bc187e6a1d861 --- /dev/null +++ b/packages/upload-media/src/components/provider/index.tsx @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { useEffect } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import withRegistryProvider from './with-registry-provider'; +import { unlock } from '../../lock-unlock'; +import { store as uploadStore } from '../../store'; + +const MediaUploadProvider = withRegistryProvider( ( props: any ) => { + const { children, settings } = props; + const { updateSettings } = unlock( useDispatch( uploadStore ) ); + + useEffect( () => { + updateSettings( settings ); + }, [ settings, updateSettings ] ); + + return <>{ children }</>; +} ); + +export default MediaUploadProvider; diff --git a/packages/upload-media/src/components/provider/with-registry-provider.tsx b/packages/upload-media/src/components/provider/with-registry-provider.tsx new file mode 100644 index 00000000000000..9a47a5601d33ed --- /dev/null +++ b/packages/upload-media/src/components/provider/with-registry-provider.tsx @@ -0,0 +1,59 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; +import { useRegistry, createRegistry, RegistryProvider } from '@wordpress/data'; +import { createHigherOrderComponent } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { storeConfig } from '../../store'; +import { STORE_NAME as mediaUploadStoreName } from '../../store/constants'; + +type WPDataRegistry = ReturnType< typeof createRegistry >; + +function getSubRegistry( + subRegistries: WeakMap< WPDataRegistry, WPDataRegistry >, + registry: WPDataRegistry, + useSubRegistry: boolean +) { + if ( ! useSubRegistry ) { + return registry; + } + let subRegistry = subRegistries.get( registry ); + if ( ! subRegistry ) { + subRegistry = createRegistry( {}, registry ); + subRegistry.registerStore( mediaUploadStoreName, storeConfig ); + subRegistries.set( registry, subRegistry ); + } + return subRegistry; +} + +const withRegistryProvider = createHigherOrderComponent( + ( WrappedComponent ) => + ( { useSubRegistry = true, ...props } ) => { + const registry = useRegistry() as unknown as WPDataRegistry; + const [ subRegistries ] = useState< + WeakMap< WPDataRegistry, WPDataRegistry > + >( () => new WeakMap() ); + const subRegistry = getSubRegistry( + subRegistries, + registry, + useSubRegistry + ); + + if ( subRegistry === registry ) { + return <WrappedComponent registry={ registry } { ...props } />; + } + + return ( + <RegistryProvider value={ subRegistry }> + <WrappedComponent registry={ subRegistry } { ...props } /> + </RegistryProvider> + ); + }, + 'withRegistryProvider' +); + +export default withRegistryProvider; diff --git a/packages/upload-media/src/get-mime-types-array.ts b/packages/upload-media/src/get-mime-types-array.ts new file mode 100644 index 00000000000000..d4940d36cd6ae5 --- /dev/null +++ b/packages/upload-media/src/get-mime-types-array.ts @@ -0,0 +1,29 @@ +/** + * Browsers may use unexpected mime types, and they differ from browser to browser. + * This function computes a flexible array of mime types from the mime type structured provided by the server. + * Converts { jpg|jpeg|jpe: "image/jpeg" } into [ "image/jpeg", "image/jpg", "image/jpeg", "image/jpe" ] + * + * @param {?Object} wpMimeTypesObject Mime type object received from the server. + * Extensions are keys separated by '|' and values are mime types associated with an extension. + * + * @return An array of mime types or null + */ +export function getMimeTypesArray( + wpMimeTypesObject?: Record< string, string > | null +) { + if ( ! wpMimeTypesObject ) { + return null; + } + return Object.entries( wpMimeTypesObject ).flatMap( + ( [ extensionsString, mime ] ) => { + const [ type ] = mime.split( '/' ); + const extensions = extensionsString.split( '|' ); + return [ + mime, + ...extensions.map( + ( extension ) => `${ type }/${ extension }` + ), + ]; + } + ); +} diff --git a/packages/upload-media/src/image-file.ts b/packages/upload-media/src/image-file.ts new file mode 100644 index 00000000000000..2c1a43ee1ab67e --- /dev/null +++ b/packages/upload-media/src/image-file.ts @@ -0,0 +1,38 @@ +/** + * ImageFile class. + * + * Small wrapper around the `File` class to hold + * information about current dimensions and original + * dimensions, in case the image was resized. + */ +export class ImageFile extends File { + width = 0; + height = 0; + originalWidth? = 0; + originalHeight? = 0; + + get wasResized() { + return ( + ( this.originalWidth || 0 ) > this.width || + ( this.originalHeight || 0 ) > this.height + ); + } + + constructor( + file: File, + width: number, + height: number, + originalWidth?: number, + originalHeight?: number + ) { + super( [ file ], file.name, { + type: file.type, + lastModified: file.lastModified, + } ); + + this.width = width; + this.height = height; + this.originalWidth = originalWidth; + this.originalHeight = originalHeight; + } +} diff --git a/packages/upload-media/src/index.ts b/packages/upload-media/src/index.ts new file mode 100644 index 00000000000000..d105c2dba90392 --- /dev/null +++ b/packages/upload-media/src/index.ts @@ -0,0 +1,11 @@ +/** + * Internal dependencies + */ +import { store as uploadStore } from './store'; + +export { uploadStore as store }; + +export { default as MediaUploadProvider } from './components/provider'; +export { UploadError } from './upload-error'; + +export type { ImageFormat } from './store/types'; diff --git a/packages/upload-media/src/lock-unlock.ts b/packages/upload-media/src/lock-unlock.ts new file mode 100644 index 00000000000000..5089cb80e4bb46 --- /dev/null +++ b/packages/upload-media/src/lock-unlock.ts @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.', + '@wordpress/upload-media' + ); diff --git a/packages/upload-media/src/store/actions.ts b/packages/upload-media/src/store/actions.ts new file mode 100644 index 00000000000000..4cc3c3e31ae0e2 --- /dev/null +++ b/packages/upload-media/src/store/actions.ts @@ -0,0 +1,183 @@ +/** + * External dependencies + */ +import { v4 as uuidv4 } from 'uuid'; + +/** + * WordPress dependencies + */ +import type { createRegistry } from '@wordpress/data'; + +type WPDataRegistry = ReturnType< typeof createRegistry >; + +/** + * Internal dependencies + */ +import type { + AdditionalData, + CancelAction, + OnBatchSuccessHandler, + OnChangeHandler, + OnErrorHandler, + OnSuccessHandler, + QueueItemId, + State, +} from './types'; +import { Type } from './types'; +import type { + addItem, + processItem, + removeItem, + revokeBlobUrls, +} from './private-actions'; +import { validateMimeType } from '../validate-mime-type'; +import { validateMimeTypeForUser } from '../validate-mime-type-for-user'; +import { validateFileSize } from '../validate-file-size'; + +type ActionCreators = { + addItem: typeof addItem; + addItems: typeof addItems; + removeItem: typeof removeItem; + processItem: typeof processItem; + cancelItem: typeof cancelItem; + revokeBlobUrls: typeof revokeBlobUrls; + < T = Record< string, unknown > >( args: T ): void; +}; + +type AllSelectors = typeof import('./selectors') & + typeof import('./private-selectors'); +type CurriedState< F > = F extends ( state: State, ...args: infer P ) => infer R + ? ( ...args: P ) => R + : F; +type Selectors = { + [ key in keyof AllSelectors ]: CurriedState< AllSelectors[ key ] >; +}; + +type ThunkArgs = { + select: Selectors; + dispatch: ActionCreators; + registry: WPDataRegistry; +}; + +interface AddItemsArgs { + files: File[]; + onChange?: OnChangeHandler; + onSuccess?: OnSuccessHandler; + onBatchSuccess?: OnBatchSuccessHandler; + onError?: OnErrorHandler; + additionalData?: AdditionalData; + allowedTypes?: string[]; +} + +/** + * Adds a new item to the upload queue. + * + * @param $0 + * @param $0.files Files + * @param [$0.onChange] Function called each time a file or a temporary representation of the file is available. + * @param [$0.onSuccess] Function called after the file is uploaded. + * @param [$0.onBatchSuccess] Function called after a batch of files is uploaded. + * @param [$0.onError] Function called when an error happens. + * @param [$0.additionalData] Additional data to include in the request. + * @param [$0.allowedTypes] Array with the types of media that can be uploaded, if unset all types are allowed. + */ +export function addItems( { + files, + onChange, + onSuccess, + onError, + onBatchSuccess, + additionalData, + allowedTypes, +}: AddItemsArgs ) { + return async ( { select, dispatch }: ThunkArgs ) => { + const batchId = uuidv4(); + for ( const file of files ) { + /* + Check if the caller (e.g. a block) supports this mime type. + Special case for file types such as HEIC which will be converted before upload anyway. + Another check will be done before upload. + */ + try { + validateMimeType( file, allowedTypes ); + validateMimeTypeForUser( + file, + select.getSettings().allowedMimeTypes + ); + } catch ( error: unknown ) { + onError?.( error as Error ); + continue; + } + + try { + validateFileSize( + file, + select.getSettings().maxUploadFileSize + ); + } catch ( error: unknown ) { + onError?.( error as Error ); + continue; + } + + dispatch.addItem( { + file, + batchId, + onChange, + onSuccess, + onBatchSuccess, + onError, + additionalData, + } ); + } + }; +} + +/** + * Cancels an item in the queue based on an error. + * + * @param id Item ID. + * @param error Error instance. + * @param silent Whether to cancel the item silently, + * without invoking its `onError` callback. + */ +export function cancelItem( id: QueueItemId, error: Error, silent = false ) { + return async ( { select, dispatch }: ThunkArgs ) => { + const item = select.getItem( id ); + + if ( ! item ) { + /* + * Do nothing if item has already been removed. + * This can happen if an upload is cancelled manually + * while transcoding with vips is still in progress. + * Then, cancelItem() is once invoked manually and once + * by the error handler in optimizeImageItem(). + */ + return; + } + + item.abortController?.abort(); + + if ( ! silent ) { + const { onError } = item; + onError?.( error ?? new Error( 'Upload cancelled' ) ); + if ( ! onError && error ) { + // TODO: Find better way to surface errors with sideloads etc. + // eslint-disable-next-line no-console -- Deliberately log errors here. + console.error( 'Upload cancelled', error ); + } + } + + dispatch< CancelAction >( { + type: Type.Cancel, + id, + error, + } ); + dispatch.removeItem( id ); + dispatch.revokeBlobUrls( id ); + + // All items of this batch were cancelled or finished. + if ( item.batchId && select.isBatchUploaded( item.batchId ) ) { + item.onBatchSuccess?.(); + } + }; +} diff --git a/packages/upload-media/src/store/constants.ts b/packages/upload-media/src/store/constants.ts new file mode 100644 index 00000000000000..ad0960cf62f46d --- /dev/null +++ b/packages/upload-media/src/store/constants.ts @@ -0,0 +1 @@ +export const STORE_NAME = 'core/upload-media'; diff --git a/packages/upload-media/src/store/index.ts b/packages/upload-media/src/store/index.ts new file mode 100644 index 00000000000000..c74f59ea7a7cf3 --- /dev/null +++ b/packages/upload-media/src/store/index.ts @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as privateSelectors from './private-selectors'; +import * as actions from './actions'; +import * as privateActions from './private-actions'; +import { unlock } from '../lock-unlock'; +import { STORE_NAME } from './constants'; + +/** + * Media upload data store configuration. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#registerStore + */ +export const storeConfig = { + reducer, + selectors, + actions, +}; + +/** + * Store definition for the media upload namespace. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore + */ +export const store = createReduxStore( STORE_NAME, { + reducer, + selectors, + actions, +} ); + +register( store ); +// @ts-ignore +unlock( store ).registerPrivateActions( privateActions ); +// @ts-ignore +unlock( store ).registerPrivateSelectors( privateSelectors ); diff --git a/packages/upload-media/src/store/private-actions.ts b/packages/upload-media/src/store/private-actions.ts new file mode 100644 index 00000000000000..a4d4ee7b99c781 --- /dev/null +++ b/packages/upload-media/src/store/private-actions.ts @@ -0,0 +1,407 @@ +/** + * External dependencies + */ +import { v4 as uuidv4 } from 'uuid'; + +/** + * WordPress dependencies + */ +import { createBlobURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; +import type { createRegistry } from '@wordpress/data'; + +type WPDataRegistry = ReturnType< typeof createRegistry >; + +/** + * Internal dependencies + */ +import { cloneFile, convertBlobToFile } from '../utils'; +import { StubFile } from '../stub-file'; +import type { + AddAction, + AdditionalData, + AddOperationsAction, + BatchId, + CacheBlobUrlAction, + OnBatchSuccessHandler, + OnChangeHandler, + OnErrorHandler, + OnSuccessHandler, + Operation, + OperationFinishAction, + OperationStartAction, + PauseQueueAction, + QueueItem, + QueueItemId, + ResumeQueueAction, + RevokeBlobUrlsAction, + Settings, + State, + UpdateSettingsAction, +} from './types'; +import { ItemStatus, OperationType, Type } from './types'; +import type { cancelItem } from './actions'; + +type ActionCreators = { + cancelItem: typeof cancelItem; + addItem: typeof addItem; + removeItem: typeof removeItem; + prepareItem: typeof prepareItem; + processItem: typeof processItem; + finishOperation: typeof finishOperation; + uploadItem: typeof uploadItem; + revokeBlobUrls: typeof revokeBlobUrls; + < T = Record< string, unknown > >( args: T ): void; +}; + +type AllSelectors = typeof import('./selectors') & + typeof import('./private-selectors'); +type CurriedState< F > = F extends ( state: State, ...args: infer P ) => infer R + ? ( ...args: P ) => R + : F; +type Selectors = { + [ key in keyof AllSelectors ]: CurriedState< AllSelectors[ key ] >; +}; + +type ThunkArgs = { + select: Selectors; + dispatch: ActionCreators; + registry: WPDataRegistry; +}; + +interface AddItemArgs { + // It should always be a File, but some consumers might still pass Blobs only. + file: File | Blob; + batchId?: BatchId; + onChange?: OnChangeHandler; + onSuccess?: OnSuccessHandler; + onError?: OnErrorHandler; + onBatchSuccess?: OnBatchSuccessHandler; + additionalData?: AdditionalData; + sourceUrl?: string; + sourceAttachmentId?: number; + abortController?: AbortController; + operations?: Operation[]; +} + +/** + * Adds a new item to the upload queue. + * + * @param $0 + * @param $0.file File + * @param [$0.batchId] Batch ID. + * @param [$0.onChange] Function called each time a file or a temporary representation of the file is available. + * @param [$0.onSuccess] Function called after the file is uploaded. + * @param [$0.onBatchSuccess] Function called after a batch of files is uploaded. + * @param [$0.onError] Function called when an error happens. + * @param [$0.additionalData] Additional data to include in the request. + * @param [$0.sourceUrl] Source URL. Used when importing a file from a URL or optimizing an existing file. + * @param [$0.sourceAttachmentId] Source attachment ID. Used when optimizing an existing file for example. + * @param [$0.abortController] Abort controller for upload cancellation. + * @param [$0.operations] List of operations to perform. Defaults to automatically determined list, based on the file. + */ +export function addItem( { + file: fileOrBlob, + batchId, + onChange, + onSuccess, + onBatchSuccess, + onError, + additionalData = {} as AdditionalData, + sourceUrl, + sourceAttachmentId, + abortController, + operations, +}: AddItemArgs ) { + return async ( { dispatch }: ThunkArgs ) => { + const itemId = uuidv4(); + + // Hardening in case a Blob is passed instead of a File. + // See https://github.com/WordPress/gutenberg/pull/65693 for an example. + const file = convertBlobToFile( fileOrBlob ); + + let blobUrl; + + // StubFile could be coming from addItemFromUrl(). + if ( ! ( file instanceof StubFile ) ) { + blobUrl = createBlobURL( file ); + dispatch< CacheBlobUrlAction >( { + type: Type.CacheBlobUrl, + id: itemId, + blobUrl, + } ); + } + + dispatch< AddAction >( { + type: Type.Add, + item: { + id: itemId, + batchId, + status: ItemStatus.Processing, + sourceFile: cloneFile( file ), + file, + attachment: { + url: blobUrl, + }, + additionalData: { + convert_format: false, + ...additionalData, + }, + onChange, + onSuccess, + onBatchSuccess, + onError, + sourceUrl, + sourceAttachmentId, + abortController: abortController || new AbortController(), + operations: Array.isArray( operations ) + ? operations + : [ OperationType.Prepare ], + }, + } ); + + dispatch.processItem( itemId ); + }; +} + +/** + * Processes a single item in the queue. + * + * Runs the next operation in line and invokes any callbacks. + * + * @param id Item ID. + */ +export function processItem( id: QueueItemId ) { + return async ( { select, dispatch }: ThunkArgs ) => { + if ( select.isPaused() ) { + return; + } + + const item = select.getItem( id ) as QueueItem; + + const { attachment, onChange, onSuccess, onBatchSuccess, batchId } = + item; + + const operation = Array.isArray( item.operations?.[ 0 ] ) + ? item.operations[ 0 ][ 0 ] + : item.operations?.[ 0 ]; + + if ( attachment ) { + onChange?.( [ attachment ] ); + } + + /* + If there are no more operations, the item can be removed from the queue, + but only if there are no thumbnails still being side-loaded, + or if itself is a side-loaded item. + */ + + if ( ! operation ) { + if ( attachment ) { + onSuccess?.( [ attachment ] ); + } + + // dispatch.removeItem( id ); + dispatch.revokeBlobUrls( id ); + + if ( batchId && select.isBatchUploaded( batchId ) ) { + onBatchSuccess?.(); + } + + /* + At this point we are dealing with a parent whose children haven't fully uploaded yet. + Do nothing and let the removal happen once the last side-loaded item finishes. + */ + + return; + } + + if ( ! operation ) { + // This shouldn't really happen. + return; + } + + dispatch< OperationStartAction >( { + type: Type.OperationStart, + id, + operation, + } ); + + switch ( operation ) { + case OperationType.Prepare: + dispatch.prepareItem( item.id ); + break; + + case OperationType.Upload: + dispatch.uploadItem( id ); + break; + } + }; +} + +/** + * Returns an action object that pauses all processing in the queue. + * + * Useful for testing purposes. + * + * @return Action object. + */ +export function pauseQueue(): PauseQueueAction { + return { + type: Type.PauseQueue, + }; +} + +/** + * Resumes all processing in the queue. + * + * Dispatches an action object for resuming the queue itself, + * and triggers processing for each remaining item in the queue individually. + */ +export function resumeQueue() { + return async ( { select, dispatch }: ThunkArgs ) => { + dispatch< ResumeQueueAction >( { + type: Type.ResumeQueue, + } ); + + for ( const item of select.getAllItems() ) { + dispatch.processItem( item.id ); + } + }; +} + +/** + * Removes a specific item from the queue. + * + * @param id Item ID. + */ +export function removeItem( id: QueueItemId ) { + return async ( { select, dispatch }: ThunkArgs ) => { + const item = select.getItem( id ); + if ( ! item ) { + return; + } + + dispatch( { + type: Type.Remove, + id, + } ); + }; +} + +/** + * Finishes an operation for a given item ID and immediately triggers processing the next one. + * + * @param id Item ID. + * @param updates Updated item data. + */ +export function finishOperation( + id: QueueItemId, + updates: Partial< QueueItem > +) { + return async ( { dispatch }: ThunkArgs ) => { + dispatch< OperationFinishAction >( { + type: Type.OperationFinish, + id, + item: updates, + } ); + + dispatch.processItem( id ); + }; +} + +/** + * Prepares an item for initial processing. + * + * Determines the list of operations to perform for a given image, + * depending on its media type. + * + * For example, HEIF images first need to be converted, resized, + * compressed, and then uploaded. + * + * Or videos need to be compressed, and then need poster generation + * before upload. + * + * @param id Item ID. + */ +export function prepareItem( id: QueueItemId ) { + return async ( { dispatch }: ThunkArgs ) => { + const operations: Operation[] = [ OperationType.Upload ]; + + dispatch< AddOperationsAction >( { + type: Type.AddOperations, + id, + operations, + } ); + + dispatch.finishOperation( id, {} ); + }; +} + +/** + * Uploads an item to the server. + * + * @param id Item ID. + */ +export function uploadItem( id: QueueItemId ) { + return async ( { select, dispatch }: ThunkArgs ) => { + const item = select.getItem( id ) as QueueItem; + + select.getSettings().mediaUpload( { + filesList: [ item.file ], + additionalData: item.additionalData, + signal: item.abortController?.signal, + onFileChange: ( [ attachment ] ) => { + if ( ! isBlobURL( attachment.url ) ) { + dispatch.finishOperation( id, { + attachment, + } ); + } + }, + onSuccess: ( [ attachment ] ) => { + dispatch.finishOperation( id, { + attachment, + } ); + }, + onError: ( error ) => { + dispatch.cancelItem( id, error ); + }, + } ); + }; +} + +/** + * Revokes all blob URLs for a given item, freeing up memory. + * + * @param id Item ID. + */ +export function revokeBlobUrls( id: QueueItemId ) { + return async ( { select, dispatch }: ThunkArgs ) => { + const blobUrls = select.getBlobUrls( id ); + + for ( const blobUrl of blobUrls ) { + revokeBlobURL( blobUrl ); + } + + dispatch< RevokeBlobUrlsAction >( { + type: Type.RevokeBlobUrls, + id, + } ); + }; +} + +/** + * Returns an action object that pauses all processing in the queue. + * + * Useful for testing purposes. + * + * @param settings + * @return Action object. + */ +export function updateSettings( + settings: Partial< Settings > +): UpdateSettingsAction { + return { + type: Type.UpdateSettings, + settings, + }; +} diff --git a/packages/upload-media/src/store/private-selectors.ts b/packages/upload-media/src/store/private-selectors.ts new file mode 100644 index 00000000000000..f2cfdbef76df86 --- /dev/null +++ b/packages/upload-media/src/store/private-selectors.ts @@ -0,0 +1,113 @@ +/** + * Internal dependencies + */ +import { + type BatchId, + ItemStatus, + OperationType, + type QueueItem, + type QueueItemId, + type State, +} from './types'; + +/** + * Returns all items currently being uploaded. + * + * @param state Upload state. + * + * @return Queue items. + */ +export function getAllItems( state: State ): QueueItem[] { + return state.queue; +} + +/** + * Returns a specific item given its unique ID. + * + * @param state Upload state. + * @param id Item ID. + * + * @return Queue item. + */ +export function getItem( + state: State, + id: QueueItemId +): QueueItem | undefined { + return state.queue.find( ( item ) => item.id === id ); +} + +/** + * Determines whether a batch has been successfully uploaded, given its unique ID. + * + * @param state Upload state. + * @param batchId Batch ID. + * + * @return Whether a batch has been uploaded. + */ +export function isBatchUploaded( state: State, batchId: BatchId ): boolean { + const batchItems = state.queue.filter( + ( item ) => batchId === item.batchId + ); + return batchItems.length === 0; +} + +/** + * Determines whether an upload is currently in progress given a post or attachment ID. + * + * @param state Upload state. + * @param postOrAttachmentId Post ID or attachment ID. + * + * @return Whether upload is currently in progress for the given post or attachment. + */ +export function isUploadingToPost( + state: State, + postOrAttachmentId: number +): boolean { + return state.queue.some( + ( item ) => + item.currentOperation === OperationType.Upload && + item.additionalData.post === postOrAttachmentId + ); +} + +/** + * Returns the next paused upload for a given post or attachment ID. + * + * @param state Upload state. + * @param postOrAttachmentId Post ID or attachment ID. + * + * @return Paused item. + */ +export function getPausedUploadForPost( + state: State, + postOrAttachmentId: number +): QueueItem | undefined { + return state.queue.find( + ( item ) => + item.status === ItemStatus.Paused && + item.additionalData.post === postOrAttachmentId + ); +} + +/** + * Determines whether uploading is currently paused. + * + * @param state Upload state. + * + * @return Whether uploading is currently paused. + */ +export function isPaused( state: State ): boolean { + return state.queueStatus === 'paused'; +} + +/** + * Returns all cached blob URLs for a given item ID. + * + * @param state Upload state. + * @param id Item ID + * + * @return List of blob URLs. + */ +export function getBlobUrls( state: State, id: QueueItemId ): string[] { + return state.blobUrls[ id ] || []; +} diff --git a/packages/upload-media/src/store/reducer.ts b/packages/upload-media/src/store/reducer.ts new file mode 100644 index 00000000000000..290a319fcbc1da --- /dev/null +++ b/packages/upload-media/src/store/reducer.ts @@ -0,0 +1,195 @@ +/** + * Internal dependencies + */ +import { + type AddAction, + type AddOperationsAction, + type CacheBlobUrlAction, + type CancelAction, + type OperationFinishAction, + type OperationStartAction, + type PauseQueueAction, + type QueueItem, + type RemoveAction, + type ResumeQueueAction, + type RevokeBlobUrlsAction, + type State, + Type, + type UnknownAction, + type UpdateSettingsAction, +} from './types'; + +const noop = () => {}; + +const DEFAULT_STATE: State = { + queue: [], + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: noop, + }, +}; + +type Action = + | AddAction + | RemoveAction + | CancelAction + | PauseQueueAction + | ResumeQueueAction + | AddOperationsAction + | OperationFinishAction + | OperationStartAction + | CacheBlobUrlAction + | RevokeBlobUrlsAction + | UpdateSettingsAction + | UnknownAction; + +function reducer( + state = DEFAULT_STATE, + action: Action = { type: Type.Unknown } +) { + switch ( action.type ) { + case Type.PauseQueue: { + return { + ...state, + queueStatus: 'paused', + }; + } + + case Type.ResumeQueue: { + return { + ...state, + queueStatus: 'active', + }; + } + + case Type.Add: + return { + ...state, + queue: [ ...state.queue, action.item ], + }; + + case Type.Cancel: + return { + ...state, + queue: state.queue.map( + ( item ): QueueItem => + item.id === action.id + ? { + ...item, + error: action.error, + } + : item + ), + }; + + case Type.Remove: + return { + ...state, + queue: state.queue.filter( ( item ) => item.id !== action.id ), + }; + + case Type.OperationStart: { + return { + ...state, + queue: state.queue.map( + ( item ): QueueItem => + item.id === action.id + ? { + ...item, + currentOperation: action.operation, + } + : item + ), + }; + } + + case Type.AddOperations: + return { + ...state, + queue: state.queue.map( ( item ): QueueItem => { + if ( item.id !== action.id ) { + return item; + } + + return { + ...item, + operations: [ + ...( item.operations || [] ), + ...action.operations, + ], + }; + } ), + }; + + case Type.OperationFinish: + return { + ...state, + queue: state.queue.map( ( item ): QueueItem => { + if ( item.id !== action.id ) { + return item; + } + + const operations = item.operations + ? item.operations.slice( 1 ) + : []; + + // Prevent an empty object if there's no attachment data. + const attachment = + item.attachment || action.item.attachment + ? { + ...item.attachment, + ...action.item.attachment, + } + : undefined; + + return { + ...item, + currentOperation: undefined, + operations, + ...action.item, + attachment, + additionalData: { + ...item.additionalData, + ...action.item.additionalData, + }, + }; + } ), + }; + + case Type.CacheBlobUrl: { + const blobUrls = state.blobUrls[ action.id ] || []; + return { + ...state, + blobUrls: { + ...state.blobUrls, + [ action.id ]: [ ...blobUrls, action.blobUrl ], + }, + }; + } + + case Type.RevokeBlobUrls: { + const newBlobUrls = { ...state.blobUrls }; + delete newBlobUrls[ action.id ]; + + return { + ...state, + blobUrls: newBlobUrls, + }; + } + + case Type.UpdateSettings: { + return { + ...state, + settings: { + ...state.settings, + ...action.settings, + }, + }; + } + } + + return state; +} + +export default reducer; diff --git a/packages/upload-media/src/store/selectors.ts b/packages/upload-media/src/store/selectors.ts new file mode 100644 index 00000000000000..8bcb8c5d63b6a7 --- /dev/null +++ b/packages/upload-media/src/store/selectors.ts @@ -0,0 +1,67 @@ +/** + * Internal dependencies + */ +import type { QueueItem, Settings, State } from './types'; + +/** + * Returns all items currently being uploaded. + * + * @param state Upload state. + * + * @return Queue items. + */ +export function getItems( state: State ): QueueItem[] { + return state.queue; +} + +/** + * Determines whether any upload is currently in progress. + * + * @param state Upload state. + * + * @return Whether any upload is currently in progress. + */ +export function isUploading( state: State ): boolean { + return state.queue.length >= 1; +} + +/** + * Determines whether an upload is currently in progress given an attachment URL. + * + * @param state Upload state. + * @param url Attachment URL. + * + * @return Whether upload is currently in progress for the given attachment. + */ +export function isUploadingByUrl( state: State, url: string ): boolean { + return state.queue.some( + ( item ) => item.attachment?.url === url || item.sourceUrl === url + ); +} + +/** + * Determines whether an upload is currently in progress given an attachment ID. + * + * @param state Upload state. + * @param attachmentId Attachment ID. + * + * @return Whether upload is currently in progress for the given attachment. + */ +export function isUploadingById( state: State, attachmentId: number ): boolean { + return state.queue.some( + ( item ) => + item.attachment?.id === attachmentId || + item.sourceAttachmentId === attachmentId + ); +} + +/** + * Returns the media upload settings. + * + * @param state Upload state. + * + * @return Settings + */ +export function getSettings( state: State ): Settings { + return state.settings; +} diff --git a/packages/upload-media/src/store/test/actions.ts b/packages/upload-media/src/store/test/actions.ts new file mode 100644 index 00000000000000..adb38ab27128e3 --- /dev/null +++ b/packages/upload-media/src/store/test/actions.ts @@ -0,0 +1,112 @@ +/** + * WordPress dependencies + */ +import { createRegistry } from '@wordpress/data'; + +type WPDataRegistry = ReturnType< typeof createRegistry >; + +/** + * Internal dependencies + */ +import { store as uploadStore } from '..'; +import { ItemStatus } from '../types'; +import { unlock } from '../../lock-unlock'; + +jest.mock( '@wordpress/blob', () => ( { + __esModule: true, + createBlobURL: jest.fn( () => 'blob:foo' ), + isBlobURL: jest.fn( ( str: string ) => str.startsWith( 'blob:' ) ), + revokeBlobURL: jest.fn(), +} ) ); + +function createRegistryWithStores() { + // Create a registry and register used stores. + const registry = createRegistry(); + // @ts-ignore + [ uploadStore ].forEach( registry.register ); + return registry; +} + +const jpegFile = new File( [ 'foo' ], 'example.jpg', { + lastModified: 1234567891, + type: 'image/jpeg', +} ); + +const mp4File = new File( [ 'foo' ], 'amazing-video.mp4', { + lastModified: 1234567891, + type: 'video/mp4', +} ); + +describe( 'actions', () => { + let registry: WPDataRegistry; + beforeEach( () => { + registry = createRegistryWithStores(); + unlock( registry.dispatch( uploadStore ) ).pauseQueue(); + } ); + + describe( 'addItem', () => { + it( 'adds an item to the queue', () => { + unlock( registry.dispatch( uploadStore ) ).addItem( { + file: jpegFile, + } ); + + expect( registry.select( uploadStore ).getItems() ).toHaveLength( + 1 + ); + expect( + registry.select( uploadStore ).getItems()[ 0 ] + ).toStrictEqual( + expect.objectContaining( { + id: expect.any( String ), + file: jpegFile, + sourceFile: jpegFile, + status: ItemStatus.Processing, + attachment: { + url: expect.stringMatching( /^blob:/ ), + }, + } ) + ); + } ); + } ); + + describe( 'addItems', () => { + it( 'adds multiple items to the queue', () => { + const onError = jest.fn(); + registry.dispatch( uploadStore ).addItems( { + files: [ jpegFile, mp4File ], + onError, + } ); + + expect( onError ).not.toHaveBeenCalled(); + expect( registry.select( uploadStore ).getItems() ).toHaveLength( + 2 + ); + expect( + registry.select( uploadStore ).getItems()[ 0 ] + ).toStrictEqual( + expect.objectContaining( { + id: expect.any( String ), + file: jpegFile, + sourceFile: jpegFile, + status: ItemStatus.Processing, + attachment: { + url: expect.stringMatching( /^blob:/ ), + }, + } ) + ); + expect( + registry.select( uploadStore ).getItems()[ 1 ] + ).toStrictEqual( + expect.objectContaining( { + id: expect.any( String ), + file: mp4File, + sourceFile: mp4File, + status: ItemStatus.Processing, + attachment: { + url: expect.stringMatching( /^blob:/ ), + }, + } ) + ); + } ); + } ); +} ); diff --git a/packages/upload-media/src/store/test/reducer.ts b/packages/upload-media/src/store/test/reducer.ts new file mode 100644 index 00000000000000..80b92e4b14c3d1 --- /dev/null +++ b/packages/upload-media/src/store/test/reducer.ts @@ -0,0 +1,279 @@ +/** + * Internal dependencies + */ +import reducer from '../reducer'; +import { + ItemStatus, + OperationType, + type QueueItem, + type State, + Type, +} from '../types'; + +describe( 'reducer', () => { + describe( `${ Type.Add }`, () => { + it( 'adds an item to the queue', () => { + const initialState: State = { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + } as QueueItem, + ], + }; + const state = reducer( initialState, { + type: Type.Add, + item: { + id: '2', + status: ItemStatus.Processing, + } as QueueItem, + } ); + + expect( state ).toEqual( { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: expect.any( Function ), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + } as QueueItem, + { + id: '2', + status: ItemStatus.Processing, + }, + ], + } ); + } ); + } ); + + describe( `${ Type.Cancel }`, () => { + it( 'removes an item from the queue', () => { + const initialState: State = { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + } as QueueItem, + { + id: '2', + status: ItemStatus.Processing, + } as QueueItem, + ], + }; + const state = reducer( initialState, { + type: Type.Cancel, + id: '2', + error: new Error(), + } ); + + expect( state ).toEqual( { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: expect.any( Function ), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + }, + { + id: '2', + status: ItemStatus.Processing, + error: expect.any( Error ), + }, + ], + } ); + } ); + } ); + + describe( `${ Type.Remove }`, () => { + it( 'removes an item from the queue', () => { + const initialState: State = { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + } as QueueItem, + { + id: '2', + status: ItemStatus.Processing, + } as QueueItem, + ], + }; + const state = reducer( initialState, { + type: Type.Remove, + id: '1', + } ); + + expect( state ).toEqual( { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: expect.any( Function ), + }, + queue: [ + { + id: '2', + status: ItemStatus.Processing, + }, + ], + } ); + } ); + } ); + + describe( `${ Type.AddOperations }`, () => { + it( 'appends operations to the list', () => { + const initialState: State = { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + operations: [ OperationType.Upload ], + } as QueueItem, + ], + }; + const state = reducer( initialState, { + type: Type.AddOperations, + id: '1', + operations: [ OperationType.Upload ], + } ); + + expect( state ).toEqual( { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: expect.any( Function ), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + operations: [ + OperationType.Upload, + OperationType.Upload, + ], + }, + ], + } ); + } ); + } ); + + describe( `${ Type.OperationStart }`, () => { + it( 'marks an item as processing', () => { + const initialState: State = { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + operations: [ OperationType.Upload ], + } as QueueItem, + { + id: '2', + status: ItemStatus.Processing, + operations: [ OperationType.Upload ], + } as QueueItem, + ], + }; + const state = reducer( initialState, { + type: Type.OperationStart, + id: '2', + operation: OperationType.Upload, + } ); + + expect( state ).toEqual( { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: expect.any( Function ), + }, + queue: [ + { + id: '1', + status: ItemStatus.Processing, + operations: [ OperationType.Upload ], + }, + { + id: '2', + status: ItemStatus.Processing, + operations: [ OperationType.Upload ], + currentOperation: OperationType.Upload, + }, + ], + } ); + } ); + } ); + + describe( `${ Type.OperationFinish }`, () => { + it( 'marks an item as processing', () => { + const initialState: State = { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + queue: [ + { + id: '1', + additionalData: {}, + attachment: {}, + status: ItemStatus.Processing, + operations: [ OperationType.Upload ], + currentOperation: OperationType.Upload, + } as QueueItem, + ], + }; + const state = reducer( initialState, { + type: Type.OperationFinish, + id: '1', + item: {}, + } ); + + expect( state ).toEqual( { + queueStatus: 'active', + blobUrls: {}, + settings: { + mediaUpload: expect.any( Function ), + }, + queue: [ + { + id: '1', + additionalData: {}, + attachment: {}, + status: ItemStatus.Processing, + currentOperation: undefined, + operations: [], + }, + ], + } ); + } ); + } ); +} ); diff --git a/packages/upload-media/src/store/test/selectors.ts b/packages/upload-media/src/store/test/selectors.ts new file mode 100644 index 00000000000000..716b7792ef77a4 --- /dev/null +++ b/packages/upload-media/src/store/test/selectors.ts @@ -0,0 +1,105 @@ +/** + * Internal dependencies + */ +import { + getItems, + isUploading, + isUploadingById, + isUploadingByUrl, +} from '../selectors'; +import { ItemStatus, type QueueItem, type State } from '../types'; + +describe( 'selectors', () => { + describe( 'getItems', () => { + it( 'should return empty array by default', () => { + const state: State = { + queue: [], + queueStatus: 'paused', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + }; + + expect( getItems( state ) ).toHaveLength( 0 ); + } ); + } ); + + describe( 'isUploading', () => { + it( 'should return true if there are items in the pipeline', () => { + const state: State = { + queue: [ + { + status: ItemStatus.Processing, + }, + { + status: ItemStatus.Processing, + }, + { + status: ItemStatus.Paused, + }, + ] as QueueItem[], + queueStatus: 'paused', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + }; + + expect( isUploading( state ) ).toBe( true ); + } ); + } ); + + describe( 'isUploadingByUrl', () => { + it( 'should return true if there are items in the pipeline', () => { + const state: State = { + queue: [ + { + status: ItemStatus.Processing, + attachment: { + url: 'https://example.com/one.jpeg', + }, + }, + { + status: ItemStatus.Processing, + }, + ] as QueueItem[], + queueStatus: 'paused', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + }; + + expect( + isUploadingByUrl( state, 'https://example.com/one.jpeg' ) + ).toBe( true ); + expect( + isUploadingByUrl( state, 'https://example.com/three.jpeg' ) + ).toBe( false ); + } ); + } ); + + describe( 'isUploadingById', () => { + it( 'should return true if there are items in the pipeline', () => { + const state: State = { + queue: [ + { + status: ItemStatus.Processing, + attachment: { + id: 123, + }, + }, + ] as QueueItem[], + queueStatus: 'paused', + blobUrls: {}, + settings: { + mediaUpload: jest.fn(), + }, + }; + + expect( isUploadingById( state, 123 ) ).toBe( true ); + expect( isUploadingById( state, 789 ) ).toBe( false ); + } ); + } ); +} ); diff --git a/packages/upload-media/src/store/types.ts b/packages/upload-media/src/store/types.ts new file mode 100644 index 00000000000000..5084e006a2cfa9 --- /dev/null +++ b/packages/upload-media/src/store/types.ts @@ -0,0 +1,172 @@ +export type QueueItemId = string; + +export type QueueStatus = 'active' | 'paused'; + +export type BatchId = string; + +export interface QueueItem { + id: QueueItemId; + sourceFile: File; + file: File; + poster?: File; + attachment?: Partial< Attachment >; + status: ItemStatus; + additionalData: AdditionalData; + onChange?: OnChangeHandler; + onSuccess?: OnSuccessHandler; + onError?: OnErrorHandler; + onBatchSuccess?: OnBatchSuccessHandler; + currentOperation?: OperationType; + operations?: Operation[]; + error?: Error; + batchId?: string; + sourceUrl?: string; + sourceAttachmentId?: number; + abortController?: AbortController; +} + +export interface State { + queue: QueueItem[]; + queueStatus: QueueStatus; + blobUrls: Record< QueueItemId, string[] >; + settings: Settings; +} + +export enum Type { + Unknown = 'REDUX_UNKNOWN', + Add = 'ADD_ITEM', + Prepare = 'PREPARE_ITEM', + Cancel = 'CANCEL_ITEM', + Remove = 'REMOVE_ITEM', + PauseItem = 'PAUSE_ITEM', + ResumeItem = 'RESUME_ITEM', + PauseQueue = 'PAUSE_QUEUE', + ResumeQueue = 'RESUME_QUEUE', + OperationStart = 'OPERATION_START', + OperationFinish = 'OPERATION_FINISH', + AddOperations = 'ADD_OPERATIONS', + CacheBlobUrl = 'CACHE_BLOB_URL', + RevokeBlobUrls = 'REVOKE_BLOB_URLS', + UpdateSettings = 'UPDATE_SETTINGS', +} + +type Action< T = Type, Payload = Record< string, unknown > > = { + type: T; +} & Payload; + +export type UnknownAction = Action< Type.Unknown >; +export type AddAction = Action< + Type.Add, + { + item: Omit< QueueItem, 'operations' > & + Partial< Pick< QueueItem, 'operations' > >; + } +>; +export type OperationStartAction = Action< + Type.OperationStart, + { id: QueueItemId; operation: OperationType } +>; +export type OperationFinishAction = Action< + Type.OperationFinish, + { + id: QueueItemId; + item: Partial< QueueItem >; + } +>; +export type AddOperationsAction = Action< + Type.AddOperations, + { id: QueueItemId; operations: Operation[] } +>; +export type CancelAction = Action< + Type.Cancel, + { id: QueueItemId; error: Error } +>; +export type PauseItemAction = Action< Type.PauseItem, { id: QueueItemId } >; +export type ResumeItemAction = Action< Type.ResumeItem, { id: QueueItemId } >; +export type PauseQueueAction = Action< Type.PauseQueue >; +export type ResumeQueueAction = Action< Type.ResumeQueue >; +export type RemoveAction = Action< Type.Remove, { id: QueueItemId } >; +export type CacheBlobUrlAction = Action< + Type.CacheBlobUrl, + { id: QueueItemId; blobUrl: string } +>; +export type RevokeBlobUrlsAction = Action< + Type.RevokeBlobUrls, + { id: QueueItemId } +>; +export type UpdateSettingsAction = Action< + Type.UpdateSettings, + { settings: Partial< Settings > } +>; + +interface UploadMediaArgs { + // Additional data to include in the request. + additionalData?: AdditionalData; + // Array with the types of media that can be uploaded, if unset all types are allowed. + allowedTypes?: string[]; + // List of files. + filesList: File[]; + // Maximum upload size in bytes allowed for the site. + maxUploadFileSize?: number; + // Function called when an error happens. + onError?: OnErrorHandler; + // Function called each time a file or a temporary representation of the file is available. + onFileChange?: OnChangeHandler; + // Function called once a file has completely finished uploading, including thumbnails. + onSuccess?: OnSuccessHandler; + // List of allowed mime types and file extensions. + wpAllowedMimeTypes?: Record< string, string > | null; + // Abort signal. + signal?: AbortSignal; +} + +export interface Settings { + // Function for uploading files to the server. + mediaUpload: ( args: UploadMediaArgs ) => void; + // List of allowed mime types and file extensions. + allowedMimeTypes?: Record< string, string > | null; + // Maximum upload file size + maxUploadFileSize?: number; +} + +// Must match the Attachment type from the media-utils package. +export interface Attachment { + id: number; + alt: string; + caption: string; + title: string; + url: string; + filename: string | null; + filesize: number | null; + media_type: 'image' | 'file'; + mime_type: string; + featured_media?: number; + missing_image_sizes?: string[]; + poster?: string; +} + +export type OnChangeHandler = ( attachments: Partial< Attachment >[] ) => void; +export type OnSuccessHandler = ( attachments: Partial< Attachment >[] ) => void; +export type OnErrorHandler = ( error: Error ) => void; +export type OnBatchSuccessHandler = () => void; + +export enum ItemStatus { + Processing = 'PROCESSING', + Paused = 'PAUSED', +} + +export enum OperationType { + Prepare = 'PREPARE', + Upload = 'UPLOAD', +} + +export interface OperationArgs {} + +type OperationWithArgs< T extends keyof OperationArgs = keyof OperationArgs > = + [ T, OperationArgs[ T ] ]; + +export type Operation = OperationType | OperationWithArgs; + +export type AdditionalData = Record< string, unknown >; + +export type ImageFormat = 'jpeg' | 'webp' | 'avif' | 'png' | 'gif'; diff --git a/packages/upload-media/src/stub-file.ts b/packages/upload-media/src/stub-file.ts new file mode 100644 index 00000000000000..f308c0d48b6f49 --- /dev/null +++ b/packages/upload-media/src/stub-file.ts @@ -0,0 +1,5 @@ +export class StubFile extends File { + constructor( fileName = 'stub-file' ) { + super( [], fileName ); + } +} diff --git a/packages/upload-media/src/test/get-file-basename.ts b/packages/upload-media/src/test/get-file-basename.ts new file mode 100644 index 00000000000000..6bf968a7643468 --- /dev/null +++ b/packages/upload-media/src/test/get-file-basename.ts @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import { getFileBasename } from '../utils'; + +describe( 'getFileBasename', () => { + it.each( [ + [ 'my-video.mp4', 'my-video' ], + [ 'my.video.mp4', 'my.video' ], + [ 'my-video', 'my-video' ], + [ '', '' ], + ] )( 'for file name %s returns basename %s', ( fileName, baseName ) => { + expect( getFileBasename( fileName ) ).toStrictEqual( baseName ); + } ); +} ); diff --git a/packages/upload-media/src/test/get-file-extension.ts b/packages/upload-media/src/test/get-file-extension.ts new file mode 100644 index 00000000000000..b26c4571be73fc --- /dev/null +++ b/packages/upload-media/src/test/get-file-extension.ts @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import { getFileExtension } from '../utils'; + +describe( 'getFileExtension', () => { + it.each( [ + [ 'my-video.mp4', 'mp4' ], + [ 'my.video.mp4', 'mp4' ], + [ 'my-video', null ], + [ '', null ], + ] )( 'for file name %s returns extension %s', ( fileName, baseName ) => { + expect( getFileExtension( fileName ) ).toStrictEqual( baseName ); + } ); +} ); diff --git a/packages/upload-media/src/test/get-file-name-from-url.ts b/packages/upload-media/src/test/get-file-name-from-url.ts new file mode 100644 index 00000000000000..6e2d497472e762 --- /dev/null +++ b/packages/upload-media/src/test/get-file-name-from-url.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import { getFileNameFromUrl } from '../utils'; + +describe( 'getFileNameFromUrl', () => { + it.each( [ + [ 'https://example.com/', 'unnamed' ], + [ 'https://example.com/photo.jpeg', 'photo.jpeg' ], + [ 'https://example.com/path/to/video.mp4', 'video.mp4' ], + ] )( 'for %s returns %s', ( url, fileName ) => { + expect( getFileNameFromUrl( url ) ).toBe( fileName ); + } ); +} ); diff --git a/packages/upload-media/src/test/get-mime-types-array.ts b/packages/upload-media/src/test/get-mime-types-array.ts new file mode 100644 index 00000000000000..156955373bd0da --- /dev/null +++ b/packages/upload-media/src/test/get-mime-types-array.ts @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import { getMimeTypesArray } from '../get-mime-types-array'; + +describe( 'getMimeTypesArray', () => { + it( 'should return null if it is "falsy" e.g: undefined or null', () => { + expect( getMimeTypesArray( null ) ).toEqual( null ); + expect( getMimeTypesArray( undefined ) ).toEqual( null ); + } ); + + it( 'should return an empty array if an empty object is passed', () => { + expect( getMimeTypesArray( {} ) ).toEqual( [] ); + } ); + + it( 'should return the type plus a new mime type with type and subtype with the extension if a type is passed', () => { + expect( getMimeTypesArray( { ext: 'chicken' } ) ).toEqual( [ + 'chicken', + 'chicken/ext', + ] ); + } ); + + it( 'should return the mime type passed and a new mime type with type and the extension as subtype', () => { + expect( getMimeTypesArray( { ext: 'chicken/ribs' } ) ).toEqual( [ + 'chicken/ribs', + 'chicken/ext', + ] ); + } ); + + it( 'should return the mime type passed and an additional mime type per extension supported', () => { + expect( getMimeTypesArray( { 'jpg|jpeg|jpe': 'image/jpeg' } ) ).toEqual( + [ 'image/jpeg', 'image/jpg', 'image/jpeg', 'image/jpe' ] + ); + } ); + + it( 'should handle multiple mime types', () => { + expect( + getMimeTypesArray( { 'ext|aaa': 'chicken/ribs', aaa: 'bbb' } ) + ).toEqual( [ + 'chicken/ribs', + 'chicken/ext', + 'chicken/aaa', + 'bbb', + 'bbb/aaa', + ] ); + } ); +} ); diff --git a/packages/upload-media/src/test/image-file.ts b/packages/upload-media/src/test/image-file.ts new file mode 100644 index 00000000000000..e48ae2df6ebcef --- /dev/null +++ b/packages/upload-media/src/test/image-file.ts @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import { ImageFile } from '../image-file'; + +describe( 'ImageFile', () => { + it( 'returns whether the file was resizes', () => { + const file = new window.File( [ 'fake_file' ], 'test.jpeg', { + type: 'image/jpeg', + } ); + + const image = new ImageFile( file, 1000, 1000, 2000, 200 ); + expect( image.wasResized ).toBe( true ); + } ); +} ); diff --git a/packages/upload-media/src/test/upload-error.ts b/packages/upload-media/src/test/upload-error.ts new file mode 100644 index 00000000000000..4d5f025ed8cf39 --- /dev/null +++ b/packages/upload-media/src/test/upload-error.ts @@ -0,0 +1,24 @@ +/** + * Internal dependencies + */ +import { UploadError } from '../upload-error'; + +describe( 'UploadError', () => { + it( 'holds error code and file name', () => { + const file = new File( [], 'example.jpg', { + lastModified: 1234567891, + type: 'image/jpeg', + } ); + + const error = new UploadError( { + code: 'some_error', + message: 'An error occurred', + file, + } ); + + expect( error ).toStrictEqual( expect.any( Error ) ); + expect( error.code ).toBe( 'some_error' ); + expect( error.message ).toBe( 'An error occurred' ); + expect( error.file ).toBe( file ); + } ); +} ); diff --git a/packages/upload-media/src/test/validate-file-size.ts b/packages/upload-media/src/test/validate-file-size.ts new file mode 100644 index 00000000000000..31d6af0e7e4a55 --- /dev/null +++ b/packages/upload-media/src/test/validate-file-size.ts @@ -0,0 +1,70 @@ +/** + * Internal dependencies + */ +import { validateFileSize } from '../validate-file-size'; +import { UploadError } from '../upload-error'; + +const imageFile = new window.File( [ 'fake_file' ], 'test.jpeg', { + type: 'image/jpeg', +} ); + +const emptyFile = new window.File( [], 'test.jpeg', { + type: 'image/jpeg', +} ); + +describe( 'validateFileSize', () => { + afterEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should error if the file is empty', () => { + expect( () => { + validateFileSize( emptyFile ); + } ).toThrow( + new UploadError( { + code: 'EMPTY_FILE', + message: 'test.jpeg: This file is empty.', + file: imageFile, + } ) + ); + } ); + + it( 'should error if the file is is greater than the maximum', () => { + expect( () => { + validateFileSize( imageFile, 2 ); + } ).toThrow( + new UploadError( { + code: 'SIZE_ABOVE_LIMIT', + message: + 'test.jpeg: This file exceeds the maximum upload size for this site.', + file: imageFile, + } ) + ); + } ); + + it( 'should not error if the file is below the limit', () => { + expect( () => { + validateFileSize( imageFile, 100 ); + } ).not.toThrow( + new UploadError( { + code: 'SIZE_ABOVE_LIMIT', + message: + 'test.jpeg: This file exceeds the maximum upload size for this site.', + file: imageFile, + } ) + ); + } ); + + it( 'should not error if there is no limit', () => { + expect( () => { + validateFileSize( imageFile ); + } ).not.toThrow( + new UploadError( { + code: 'SIZE_ABOVE_LIMIT', + message: + 'test.jpeg: This file exceeds the maximum upload size for this site.', + file: imageFile, + } ) + ); + } ); +} ); diff --git a/packages/upload-media/src/test/validate-mime-type-for-user.ts b/packages/upload-media/src/test/validate-mime-type-for-user.ts new file mode 100644 index 00000000000000..d2566566862142 --- /dev/null +++ b/packages/upload-media/src/test/validate-mime-type-for-user.ts @@ -0,0 +1,37 @@ +/** + * Internal dependencies + */ +import { validateMimeTypeForUser } from '../validate-mime-type-for-user'; +import { UploadError } from '../upload-error'; + +const imageFile = new window.File( [ 'fake_file' ], 'test.jpeg', { + type: 'image/jpeg', +} ); + +describe( 'validateMimeTypeForUser', () => { + afterEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should not error if wpAllowedMimeTypes is null or missing', async () => { + expect( () => { + validateMimeTypeForUser( imageFile ); + } ).not.toThrow(); + expect( () => { + validateMimeTypeForUser( imageFile, null ); + } ).not.toThrow(); + } ); + + it( 'should error if file type is not allowed for user', async () => { + expect( () => { + validateMimeTypeForUser( imageFile, { aac: 'audio/aac' } ); + } ).toThrow( + new UploadError( { + code: 'MIME_TYPE_NOT_ALLOWED_FOR_USER', + message: + 'test.jpeg: Sorry, you are not allowed to upload this file type.', + file: imageFile, + } ) + ); + } ); +} ); diff --git a/packages/upload-media/src/test/validate-mime-type.ts b/packages/upload-media/src/test/validate-mime-type.ts new file mode 100644 index 00000000000000..a83cdcefe5f99a --- /dev/null +++ b/packages/upload-media/src/test/validate-mime-type.ts @@ -0,0 +1,57 @@ +/** + * Internal dependencies + */ +import { validateMimeType } from '../validate-mime-type'; +import { UploadError } from '../upload-error'; + +const xmlFile = new window.File( [ 'fake_file' ], 'test.xml', { + type: 'text/xml', +} ); +const imageFile = new window.File( [ 'fake_file' ], 'test.jpeg', { + type: 'image/jpeg', +} ); + +describe( 'validateMimeType', () => { + afterEach( () => { + jest.clearAllMocks(); + } ); + + it( 'should error if allowedTypes contains a partial mime type and the validation fails', async () => { + expect( () => { + validateMimeType( xmlFile, [ 'image' ] ); + } ).toThrow( + new UploadError( { + code: 'MIME_TYPE_NOT_SUPPORTED', + message: + 'test.xml: Sorry, this file type is not supported here.', + file: xmlFile, + } ) + ); + } ); + + it( 'should error if allowedTypes contains a complete mime type and the validation fails', async () => { + expect( () => { + validateMimeType( imageFile, [ 'image/gif' ] ); + } ).toThrow( + new UploadError( { + code: 'MIME_TYPE_NOT_SUPPORTED', + message: + 'test.jpeg: Sorry, this file type is not supported here.', + file: xmlFile, + } ) + ); + } ); + + it( 'should error if allowedTypes contains multiple types and the validation fails', async () => { + expect( () => { + validateMimeType( xmlFile, [ 'video', 'image' ] ); + } ).toThrow( + new UploadError( { + code: 'MIME_TYPE_NOT_SUPPORTED', + message: + 'test.xml: Sorry, this file type is not supported here.', + file: xmlFile, + } ) + ); + } ); +} ); diff --git a/packages/upload-media/src/upload-error.ts b/packages/upload-media/src/upload-error.ts new file mode 100644 index 00000000000000..d712e9dcdb6966 --- /dev/null +++ b/packages/upload-media/src/upload-error.ts @@ -0,0 +1,26 @@ +interface UploadErrorArgs { + code: string; + message: string; + file: File; + cause?: Error; +} + +/** + * MediaError class. + * + * Small wrapper around the `Error` class + * to hold an error code and a reference to a file object. + */ +export class UploadError extends Error { + code: string; + file: File; + + constructor( { code, message, file, cause }: UploadErrorArgs ) { + super( message, { cause } ); + + Object.setPrototypeOf( this, new.target.prototype ); + + this.code = code; + this.file = file; + } +} diff --git a/packages/upload-media/src/utils.ts b/packages/upload-media/src/utils.ts new file mode 100644 index 00000000000000..3950ec03887928 --- /dev/null +++ b/packages/upload-media/src/utils.ts @@ -0,0 +1,90 @@ +/** + * WordPress dependencies + */ +import { getFilename } from '@wordpress/url'; +import { _x } from '@wordpress/i18n'; + +/** + * Converts a Blob to a File with a default name like "image.png". + * + * If it is already a File object, it is returned unchanged. + * + * @param fileOrBlob Blob object. + * @return File object. + */ +export function convertBlobToFile( fileOrBlob: Blob | File ): File { + if ( fileOrBlob instanceof File ) { + return fileOrBlob; + } + + // Extension is only an approximation. + // The server will override it if incorrect. + const ext = fileOrBlob.type.split( '/' )[ 1 ]; + const mediaType = + 'application/pdf' === fileOrBlob.type + ? 'document' + : fileOrBlob.type.split( '/' )[ 0 ]; + return new File( [ fileOrBlob ], `${ mediaType }.${ ext }`, { + type: fileOrBlob.type, + } ); +} + +/** + * Renames a given file and returns a new file. + * + * Copies over the last modified time. + * + * @param file File object. + * @param name File name. + * @return Renamed file object. + */ +export function renameFile( file: File, name: string ): File { + return new File( [ file ], name, { + type: file.type, + lastModified: file.lastModified, + } ); +} + +/** + * Clones a given file object. + * + * @param file File object. + * @return New file object. + */ +export function cloneFile( file: File ): File { + return renameFile( file, file.name ); +} + +/** + * Returns the file extension from a given file name or URL. + * + * @param file File URL. + * @return File extension or null if it does not have one. + */ +export function getFileExtension( file: string ): string | null { + return file.includes( '.' ) ? file.split( '.' ).pop() || null : null; +} + +/** + * Returns file basename without extension. + * + * For example, turns "my-awesome-file.jpeg" into "my-awesome-file". + * + * @param name File name. + * @return File basename. + */ +export function getFileBasename( name: string ): string { + return name.includes( '.' ) + ? name.split( '.' ).slice( 0, -1 ).join( '.' ) + : name; +} + +/** + * Returns the file name including extension from a URL. + * + * @param url File URL. + * @return File name. + */ +export function getFileNameFromUrl( url: string ) { + return getFilename( url ) || _x( 'unnamed', 'file name' ); +} diff --git a/packages/upload-media/src/validate-file-size.ts b/packages/upload-media/src/validate-file-size.ts new file mode 100644 index 00000000000000..cc34462b268dda --- /dev/null +++ b/packages/upload-media/src/validate-file-size.ts @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { UploadError } from './upload-error'; + +/** + * Verifies whether the file is within the file upload size limits for the site. + * + * @param file File object. + * @param maxUploadFileSize Maximum upload size in bytes allowed for the site. + */ +export function validateFileSize( file: File, maxUploadFileSize?: number ) { + // Don't allow empty files to be uploaded. + if ( file.size <= 0 ) { + throw new UploadError( { + code: 'EMPTY_FILE', + message: sprintf( + // translators: %s: file name. + __( '%s: This file is empty.' ), + file.name + ), + file, + } ); + } + + if ( maxUploadFileSize && file.size > maxUploadFileSize ) { + throw new UploadError( { + code: 'SIZE_ABOVE_LIMIT', + message: sprintf( + // translators: %s: file name. + __( + '%s: This file exceeds the maximum upload size for this site.' + ), + file.name + ), + file, + } ); + } +} diff --git a/packages/upload-media/src/validate-mime-type-for-user.ts b/packages/upload-media/src/validate-mime-type-for-user.ts new file mode 100644 index 00000000000000..858c583561978e --- /dev/null +++ b/packages/upload-media/src/validate-mime-type-for-user.ts @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { UploadError } from './upload-error'; +import { getMimeTypesArray } from './get-mime-types-array'; + +/** + * Verifies if the user is allowed to upload this mime type. + * + * @param file File object. + * @param wpAllowedMimeTypes List of allowed mime types and file extensions. + */ +export function validateMimeTypeForUser( + file: File, + wpAllowedMimeTypes?: Record< string, string > | null +) { + // Allowed types for the current WP_User. + const allowedMimeTypesForUser = getMimeTypesArray( wpAllowedMimeTypes ); + + if ( ! allowedMimeTypesForUser ) { + return; + } + + const isAllowedMimeTypeForUser = allowedMimeTypesForUser.includes( + file.type + ); + + if ( file.type && ! isAllowedMimeTypeForUser ) { + throw new UploadError( { + code: 'MIME_TYPE_NOT_ALLOWED_FOR_USER', + message: sprintf( + // translators: %s: file name. + __( + '%s: Sorry, you are not allowed to upload this file type.' + ), + file.name + ), + file, + } ); + } +} diff --git a/packages/upload-media/src/validate-mime-type.ts b/packages/upload-media/src/validate-mime-type.ts new file mode 100644 index 00000000000000..2d99455d7b60f1 --- /dev/null +++ b/packages/upload-media/src/validate-mime-type.ts @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { UploadError } from './upload-error'; + +/** + * Verifies if the caller (e.g. a block) supports this mime type. + * + * @param file File object. + * @param allowedTypes List of allowed mime types. + */ +export function validateMimeType( file: File, allowedTypes?: string[] ) { + if ( ! allowedTypes ) { + return; + } + + // Allowed type specified by consumer. + const isAllowedType = allowedTypes.some( ( allowedType ) => { + // If a complete mimetype is specified verify if it matches exactly the mime type of the file. + if ( allowedType.includes( '/' ) ) { + return allowedType === file.type; + } + // Otherwise a general mime type is used, and we should verify if the file mimetype starts with it. + return file.type.startsWith( `${ allowedType }/` ); + } ); + + if ( file.type && ! isAllowedType ) { + throw new UploadError( { + code: 'MIME_TYPE_NOT_SUPPORTED', + message: sprintf( + // translators: %s: file name. + __( '%s: Sorry, this file type is not supported here.' ), + file.name + ), + file, + } ); + } +} diff --git a/packages/upload-media/tsconfig.json b/packages/upload-media/tsconfig.json new file mode 100644 index 00000000000000..df9f913b1e11b7 --- /dev/null +++ b/packages/upload-media/tsconfig.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": [ "gutenberg-env" ] + }, + "references": [ + { "path": "../api-fetch" }, + { "path": "../blob" }, + { "path": "../compose" }, + { "path": "../data" }, + { "path": "../element" }, + { "path": "../i18n" }, + { "path": "../private-apis" }, + { "path": "../url" } + ] +} diff --git a/packages/url/tsconfig.json b/packages/url/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/url/tsconfig.json +++ b/packages/url/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/vips/tsconfig.json b/packages/vips/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/vips/tsconfig.json +++ b/packages/vips/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/packages/warning/tsconfig.json b/packages/warning/tsconfig.json index 9e3edfe0ae443c..f197b56919708b 100644 --- a/packages/warning/tsconfig.json +++ b/packages/warning/tsconfig.json @@ -2,9 +2,6 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types", "types": [ "gutenberg-env" ] - }, - "include": [ "src/**/*" ] + } } diff --git a/packages/wordcount/tsconfig.json b/packages/wordcount/tsconfig.json index 6e33d8ff82d47e..7ff060ab6ce105 100644 --- a/packages/wordcount/tsconfig.json +++ b/packages/wordcount/tsconfig.json @@ -1,9 +1,4 @@ { "$schema": "https://json.schemastore.org/tsconfig.json", - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ] + "extends": "../../tsconfig.base.json" } diff --git a/patches/storybook-source-link+2.0.9.patch b/patches/storybook-source-link+2.0.9.patch new file mode 100644 index 00000000000000..c1db218d953a90 --- /dev/null +++ b/patches/storybook-source-link+2.0.9.patch @@ -0,0 +1,55 @@ +diff --git a/node_modules/storybook-source-link/dist/esm/Tool.js b/node_modules/storybook-source-link/dist/esm/Tool.js +index 100099e..53d37c4 100644 +--- a/node_modules/storybook-source-link/dist/esm/Tool.js ++++ b/node_modules/storybook-source-link/dist/esm/Tool.js +@@ -1,7 +1,8 @@ + import React from "react"; +-import { Icons, IconButton, TooltipMessage, WithTooltip } from "@storybook/components"; +-import { PARAM_KEY, PREFIX_PARAM_KEY, ICON_PARAM_KEY, INFO_LINK, TOOL_ID } from "./constants"; +-import { useParameter } from '@storybook/api'; ++import { IconButton, TooltipMessage, WithTooltip } from "@storybook/components"; ++import { RepoIcon } from '@storybook/icons'; ++import { PARAM_KEY, PREFIX_PARAM_KEY, INFO_LINK, TOOL_ID } from "./constants"; ++import { useParameter } from '@storybook/manager-api'; + + var Tooltip = function Tooltip() { + return /*#__PURE__*/React.createElement(TooltipMessage, { +@@ -24,7 +25,6 @@ export var getLink = function getLink(prefix, link) { + export var Tool = function Tool() { + var param_link = useParameter(PARAM_KEY, null); + var param_prefix = useParameter(PREFIX_PARAM_KEY, null); +- var param_icon = useParameter(ICON_PARAM_KEY, "repository"); + var link = getLink(param_prefix, param_link); + return link ? /*#__PURE__*/React.createElement(IconButton, { + key: TOOL_ID, +@@ -35,9 +35,7 @@ export var Tool = function Tool() { + window.open(link); + } + } +- }, /*#__PURE__*/React.createElement(Icons, { +- icon: param_icon +- })) : /*#__PURE__*/React.createElement(WithTooltip, { ++ }, /*#__PURE__*/React.createElement(RepoIcon)) : /*#__PURE__*/React.createElement(WithTooltip, { + placement: "top", + trigger: "click", + tooltip: /*#__PURE__*/React.createElement(Tooltip, null) +@@ -45,7 +43,5 @@ export var Tool = function Tool() { + key: TOOL_ID, + title: "View Source Repository", + active: false +- }, /*#__PURE__*/React.createElement(Icons, { +- icon: param_icon +- }))); ++ }, /*#__PURE__*/React.createElement(RepoIcon))); + }; +\ No newline at end of file +diff --git a/node_modules/storybook-source-link/dist/esm/preset/manager.js b/node_modules/storybook-source-link/dist/esm/preset/manager.js +index 845f81d..ca1d066 100644 +--- a/node_modules/storybook-source-link/dist/esm/preset/manager.js ++++ b/node_modules/storybook-source-link/dist/esm/preset/manager.js +@@ -1,4 +1,4 @@ +-import { addons, types } from "@storybook/addons"; ++import { addons, types } from "@storybook/manager-api"; + import { ADDON_ID, TOOL_ID } from "../constants"; + import { Tool } from "../Tool"; // Register the addon + diff --git a/phpunit/block-supports/border-test.php b/phpunit/block-supports/border-test.php index 510633b48aab59..858e4e92cc1740 100644 --- a/phpunit/block-supports/border-test.php +++ b/phpunit/block-supports/border-test.php @@ -128,11 +128,11 @@ public function test_flat_border_with_skipped_serialization() { 'test/flat-border-with-skipped-serialization', array( '__experimentalBorder' => array( - 'color' => true, - 'radius' => true, - 'width' => true, - 'style' => true, - 'skipSerialization' => true, + 'color' => true, + 'radius' => true, + 'width' => true, + 'style' => true, + '__experimentalSkipSerialization' => true, ), ) ); @@ -459,375 +459,4 @@ public function test_split_borders_with_named_colors() { $this->assertSame( $expected, $actual ); } - /** - * Tests that stabilized border supports will also apply to blocks using - * the experimental syntax, for backwards compatibility with existing blocks. - * - * @covers ::gutenberg_apply_border_support - */ - public function test_should_apply_experimental_border_supports() { - $this->test_block_name = 'test/experimental-border-supports'; - register_block_type( - $this->test_block_name, - array( - 'api_version' => 3, - 'attributes' => array( - 'style' => array( - 'type' => 'object', - ), - ), - 'supports' => array( - '__experimentalBorder' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - '__experimentalDefaultControls' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - ), - ), - ), - ) - ); - $registry = WP_Block_Type_Registry::get_instance(); - $block_type = $registry->get_registered( $this->test_block_name ); - $block_atts = array( - 'style' => array( - 'border' => array( - 'color' => '#72aee6', - 'radius' => '10px', - 'style' => 'dashed', - 'width' => '2px', - ), - ), - ); - - $actual = gutenberg_apply_border_support( $block_type, $block_atts ); - $expected = array( - 'class' => 'has-border-color', - 'style' => 'border-color:#72aee6;border-radius:10px;border-style:dashed;border-width:2px;', - ); - - $this->assertSame( $expected, $actual ); - } - - /** - * Tests that stabilized border supports are applied correctly. - * - * @covers ::gutenberg_apply_border_support - */ - public function test_should_apply_stabilized_border_supports() { - $this->test_block_name = 'test/stabilized-border-supports'; - register_block_type( - $this->test_block_name, - array( - 'api_version' => 3, - 'attributes' => array( - 'style' => array( - 'type' => 'object', - ), - ), - 'supports' => array( - 'border' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - '__experimentalDefaultControls' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - ), - ), - ), - ) - ); - $registry = WP_Block_Type_Registry::get_instance(); - $block_type = $registry->get_registered( $this->test_block_name ); - $block_atts = array( - 'style' => array( - 'border' => array( - 'color' => '#72aee6', - 'radius' => '10px', - 'style' => 'dashed', - 'width' => '2px', - ), - ), - ); - - $actual = gutenberg_apply_border_support( $block_type, $block_atts ); - $expected = array( - 'class' => 'has-border-color', - 'style' => 'border-color:#72aee6;border-radius:10px;border-style:dashed;border-width:2px;', - ); - - $this->assertSame( $expected, $actual ); - } - - /** - * Tests that experimental border support configuration gets stabilized correctly. - */ - public function test_should_stabilize_border_supports() { - $block_type_args = array( - 'supports' => array( - '__experimentalBorder' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - '__experimentalSkipSerialization' => true, - '__experimentalDefaultControls' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - ), - ), - ), - ); - - $actual = gutenberg_stabilize_experimental_block_supports( $block_type_args ); - $expected = array( - 'supports' => array( - 'border' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - 'skipSerialization' => true, - // Has to be kept due to core's `wp_should_skip_block_supports_serialization` only checking the experimental flag until 6.8. - '__experimentalSkipSerialization' => true, - 'defaultControls' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - ), - ), - ), - ); - - $this->assertSame( $expected, $actual, 'Stabilized border block support config does not match.' ); - } - - /** - * Tests the merging of border support configuration when stabilizing - * experimental config. Due to the ability to filter block type args, plugins - * or themes could filter using outdated experimental keys. While not every - * permutation of filtering can be covered, the majority of use cases are - * served best by merging configs based on the order they were defined if possible. - */ - public function test_should_stabilize_border_supports_using_order_based_merge() { - $experimental_border_config = array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - '__experimentalSkipSerialization' => true, - '__experimentalDefaultControls' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - ), - - /* - * The following simulates theme/plugin filtering using `__experimentalBorder` - * key but stable serialization and default control keys. - */ - 'skipSerialization' => false, - 'defaultControls' => array( - 'color' => true, - 'radius' => false, - 'style' => true, - 'width' => true, - ), - ); - $stable_border_config = array( - 'color' => true, - 'radius' => true, - 'style' => false, - 'width' => true, - 'skipSerialization' => false, - 'defaultControls' => array( - 'color' => true, - 'radius' => false, - 'style' => false, - 'width' => true, - ), - - /* - * The following simulates theme/plugin filtering using stable `border` key - * but experimental serialization and default control keys. - */ - '__experimentalSkipSerialization' => true, - '__experimentalDefaultControls' => array( - 'color' => false, - 'radius' => false, - 'style' => false, - 'width' => false, - ), - ); - - $experimental_first_args = array( - 'supports' => array( - '__experimentalBorder' => $experimental_border_config, - 'border' => $stable_border_config, - ), - ); - - $actual = gutenberg_stabilize_experimental_block_supports( $experimental_first_args ); - $expected = array( - 'supports' => array( - 'border' => array( - 'color' => true, - 'radius' => true, - 'style' => false, - 'width' => true, - 'skipSerialization' => true, - '__experimentalSkipSerialization' => true, - 'defaultControls' => array( - 'color' => false, - 'radius' => false, - 'style' => false, - 'width' => false, - ), - - ), - ), - ); - $this->assertSame( $expected, $actual, 'Merged stabilized border block support config does not match when experimental keys are first.' ); - - $stable_first_args = array( - 'supports' => array( - 'border' => $stable_border_config, - '__experimentalBorder' => $experimental_border_config, - ), - ); - - $actual = gutenberg_stabilize_experimental_block_supports( $stable_first_args ); - $expected = array( - 'supports' => array( - 'border' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - 'skipSerialization' => false, - '__experimentalSkipSerialization' => false, - 'defaultControls' => array( - 'color' => true, - 'radius' => false, - 'style' => true, - 'width' => true, - ), - ), - ), - ); - $this->assertSame( $expected, $actual, 'Merged stabilized border block support config does not match when stable keys are first.' ); - } - - /** - * Tests that boolean border support configurations are handled correctly. - * - * @dataProvider data_boolean_border_supports - * - * @param array $supports The supports configuration to test. - * @param boolean|array $expected_value The expected final border support value. - */ - public function test_should_handle_boolean_border_supports( $supports, $expected_value ) { - $args = array( - 'supports' => $supports, - ); - - $actual = gutenberg_stabilize_experimental_block_supports( $args ); - - $this->assertSame( $expected_value, $actual['supports']['border'] ); - } - - /** - * Data provider for boolean border support tests. - * - * @return array Test parameters. - */ - public function data_boolean_border_supports() { - return array( - 'experimental true only' => array( - array( - '__experimentalBorder' => true, - ), - true, - ), - 'experimental false only' => array( - array( - '__experimentalBorder' => false, - ), - false, - ), - 'experimental true before stable false' => array( - array( - '__experimentalBorder' => true, - 'border' => false, - ), - false, - ), - 'stable true before experimental false' => array( - array( - 'border' => true, - '__experimentalBorder' => false, - ), - false, - ), - 'experimental array before stable boolean' => array( - array( - '__experimentalBorder' => array( - 'color' => true, - 'width' => true, - ), - 'border' => false, - ), - false, - ), - 'stable array before experimental boolean' => array( - array( - 'border' => array( - 'color' => true, - 'width' => true, - ), - '__experimentalBorder' => true, - ), - true, - ), - 'experimental boolean before stable array' => array( - array( - '__experimentalBorder' => true, - 'border' => array( - 'color' => true, - 'width' => true, - ), - ), - array( - 'color' => true, - 'width' => true, - ), - ), - 'stable boolean before experimental array' => array( - array( - 'border' => false, - '__experimentalBorder' => array( - 'color' => true, - 'width' => true, - ), - ), - array( - 'color' => true, - 'width' => true, - ), - ), - ); - } } diff --git a/phpunit/block-supports/typography-test.php b/phpunit/block-supports/typography-test.php index 1804659c11af3c..eafd505db6ec65 100644 --- a/phpunit/block-supports/typography-test.php +++ b/phpunit/block-supports/typography-test.php @@ -283,111 +283,6 @@ public function test_should_generate_classname_for_font_family() { $this->assertSame( $expected, $actual ); } - /** - * Tests that stabilized typography supports will also apply to blocks using - * the experimental syntax, for backwards compatibility with existing blocks. - * - * @covers ::gutenberg_apply_typography_support - */ - public function test_should_apply_experimental_typography_supports() { - $this->test_block_name = 'test/experimental-typography-supports'; - register_block_type( - $this->test_block_name, - array( - 'api_version' => 3, - 'attributes' => array( - 'style' => array( - 'type' => 'object', - ), - ), - 'supports' => array( - 'typography' => array( - '__experimentalFontFamily' => true, - '__experimentalFontStyle' => true, - '__experimentalFontWeight' => true, - '__experimentalLetterSpacing' => true, - '__experimentalTextDecoration' => true, - '__experimentalTextTransform' => true, - ), - ), - ) - ); - $registry = WP_Block_Type_Registry::get_instance(); - $block_type = $registry->get_registered( $this->test_block_name ); - $block_atts = array( - 'fontFamily' => 'serif', - 'style' => array( - 'typography' => array( - 'fontStyle' => 'italic', - 'fontWeight' => 'bold', - 'letterSpacing' => '1px', - 'textDecoration' => 'underline', - 'textTransform' => 'uppercase', - ), - ), - ); - - $actual = gutenberg_apply_typography_support( $block_type, $block_atts ); - $expected = array( - 'class' => 'has-serif-font-family', - 'style' => 'font-style:italic;font-weight:bold;text-decoration:underline;text-transform:uppercase;letter-spacing:1px;', - ); - - $this->assertSame( $expected, $actual ); - } - - /** - * Tests that stabilized typography supports are applied correctly. - * - * @covers ::gutenberg_apply_typography_support - */ - public function test_should_apply_stabilized_typography_supports() { - $this->test_block_name = 'test/experimental-typography-supports'; - register_block_type( - $this->test_block_name, - array( - 'api_version' => 3, - 'attributes' => array( - 'style' => array( - 'type' => 'object', - ), - ), - 'supports' => array( - 'typography' => array( - 'fontFamily' => true, - 'fontStyle' => true, - 'fontWeight' => true, - 'letterSpacing' => true, - 'textDecoration' => true, - 'textTransform' => true, - ), - ), - ) - ); - $registry = WP_Block_Type_Registry::get_instance(); - $block_type = $registry->get_registered( $this->test_block_name ); - $block_atts = array( - 'fontFamily' => 'serif', - 'style' => array( - 'typography' => array( - 'fontStyle' => 'italic', - 'fontWeight' => 'bold', - 'letterSpacing' => '1px', - 'textDecoration' => 'underline', - 'textTransform' => 'uppercase', - ), - ), - ); - - $actual = gutenberg_apply_typography_support( $block_type, $block_atts ); - $expected = array( - 'class' => 'has-serif-font-family', - 'style' => 'font-style:italic;font-weight:bold;text-decoration:underline;text-transform:uppercase;letter-spacing:1px;', - ); - - $this->assertSame( $expected, $actual ); - } - /** * Tests generating font size values, including fluid formulae, from fontSizes preset. * diff --git a/phpunit/class-gutenberg-hierarchical-sort-test.php b/phpunit/class-gutenberg-hierarchical-sort-test.php new file mode 100644 index 00000000000000..31b78b272a29a2 --- /dev/null +++ b/phpunit/class-gutenberg-hierarchical-sort-test.php @@ -0,0 +1,207 @@ +<?php + +/** + * Test the build_post_ids_to_display function. + * + * @package Gutenberg + */ +class GutenbergHierarchicalSortTest extends WP_UnitTestCase { + + public function test_return_all_post_ids() { + /* + * Keep this updated as the input array changes. + * The sorted hierarchy would be as follows: + * + * 12 + * - 3 + * -- 6 + * -- 5 + * - 4 + * -- 7 + * 8 + * - 9 + * -- 11 + * - 10 + * + */ + $input = array( + (object) array( + 'ID' => 11, + 'post_parent' => 9, + ), + (object) array( + 'ID' => 12, + 'post_parent' => 0, + ), + (object) array( + 'ID' => 8, + 'post_parent' => 0, + ), + (object) array( + 'ID' => 3, + 'post_parent' => 12, + ), + (object) array( + 'ID' => 6, + 'post_parent' => 3, + ), + (object) array( + 'ID' => 7, + 'post_parent' => 4, + ), + (object) array( + 'ID' => 9, + 'post_parent' => 8, + ), + (object) array( + 'ID' => 4, + 'post_parent' => 12, + ), + (object) array( + 'ID' => 5, + 'post_parent' => 3, + ), + (object) array( + 'ID' => 10, + 'post_parent' => 8, + ), + ); + + $hs = Gutenberg_Hierarchical_Sort::get_instance(); + $result = $hs->sort( $input ); + $this->assertEquals( array( 12, 3, 6, 5, 4, 7, 8, 9, 11, 10 ), $result['post_ids'] ); + $this->assertEquals( + array( + 12 => 0, + 3 => 1, + 6 => 2, + 5 => 2, + 4 => 1, + 7 => 2, + 8 => 0, + 9 => 1, + 11 => 2, + 10 => 1, + ), + $result['levels'] + ); + } + + public function test_return_orphans() { + /* + * Keep this updated as the input array changes. + * The sorted hierarchy would be as follows: + * + * - 11 (orphan) + * - 4 (orphan) + * -- 7 + * + */ + $input = array( + (object) array( + 'ID' => 11, + 'post_parent' => 2, + ), + (object) array( + 'ID' => 7, + 'post_parent' => 4, + ), + (object) array( + 'ID' => 4, + 'post_parent' => 2, + ), + ); + + $hs = Gutenberg_Hierarchical_Sort::get_instance(); + $result = $hs->sort( $input ); + $this->assertEquals( array( 11, 4, 7 ), $result['post_ids'] ); + $this->assertEquals( + array( + 11 => 1, + 4 => 1, + 7 => 2, + ), + $result['levels'] + ); + } + + public function test_post_with_empty_post_parent_are_considered_top_level() { + /* + * Keep this updated as the input array changes. + * The sorted hierarchy would be as follows: + * + * 2 + * - 3 + * -- 5 + * -- 6 + * - 4 + * -- 7 + * 8 + * - 9 + * -- 11 + * - 10 + * + */ + $input = array( + (object) array( + 'ID' => 11, + 'post_parent' => 9, + ), + (object) array( + 'ID' => 2, + 'post_parent' => 0, + ), + (object) array( + 'ID' => 8, + 'post_parent' => '', // Empty post parent, should be considered top-level. + ), + (object) array( + 'ID' => 3, + 'post_parent' => 2, + ), + (object) array( + 'ID' => 5, + 'post_parent' => 3, + ), + (object) array( + 'ID' => 7, + 'post_parent' => 4, + ), + (object) array( + 'ID' => 9, + 'post_parent' => 8, + ), + (object) array( + 'ID' => 4, + 'post_parent' => 2, + ), + (object) array( + 'ID' => 6, + 'post_parent' => 3, + ), + (object) array( + 'ID' => 10, + 'post_parent' => 8, + ), + ); + + $hs = Gutenberg_Hierarchical_Sort::get_instance(); + $result = $hs->sort( $input ); + $this->assertEquals( array( 2, 3, 5, 6, 4, 7, 8, 9, 11, 10 ), $result['post_ids'] ); + $this->assertEquals( + array( + 2 => 0, + 3 => 1, + 5 => 2, + 6 => 2, + 4 => 1, + 7 => 2, + 8 => 0, + 9 => 1, + 11 => 2, + 10 => 1, + ), + $result['levels'] + ); + } +} diff --git a/platform-docs/docs/intro.md b/platform-docs/docs/intro.md index 05cfa103492f33..48f1410871ccef 100644 --- a/platform-docs/docs/intro.md +++ b/platform-docs/docs/intro.md @@ -4,17 +4,16 @@ sidebar_position: 1 # Getting Started -Let's discover how to use the **Gutenberg Block Editor** to build your own block editor in less than 10 minutes**. - +Let's discover how to use the **Gutenberg Block Editor** to build your own block editor in less than 10 minutes. ## What you'll need - [Node.js](https://nodejs.org/en/download/) version 20.10 or above. -- We're going to be using "vite" to setup our single page application (SPA) that contains a block editor. You can use your own setup, and your own application for this. +- We're going to be using "Vite" to setup our single page application (SPA) that contains a block editor. You can use your own setup, and your own application for this. ## Preparing the SPA powered by Vite. -First bootstrap a vite project using `npm create vite@latest` and pick `Vanilla` variant and `JavaScript` as a language. +First bootstrap a Vite project using `npm create vite@latest` and pick `React` variant and `JavaScript` as a language. Once done, you can navigate to your application folder and run it locally using `npm run dev`. Open the displayed local URL in a browser. @@ -28,58 +27,72 @@ To build a block editor, you need to install the following dependencies: ## JSX -We're going to be using JSX to write our UI and components. So one of the first steps we need to do is to configure our build tooling, By default vite supports JSX and and outputs the result as a React pragma. The Block editor uses React so there's no need to configure anything here but if you're using a different bundler/build tool, make sure the JSX transpilation is setup properly. +We're going to be using JSX to write our UI and components as the block editor is built with React. Using the Vite bootstrap described above there’s no need to configure anything as it outputs the result as a React pragma. If you're using a different bundler/build tool, you may need to configure the JSX transpilation to do the same. ## Bootstrap your block editor -It's time to render our first block editor. +It's time to render our first block editor. We’ll do this with changes to three files – `index.html`, `src/main.jsx`, and `src/App.jsx`. + +First, we’ll add the base styles are for the editor UI. In `index.html` add these styles in the `<head>`: +```html +<link href="node_modules/@wordpress/components/build-style/style.css" rel="stylesheet" vite-ignore/> +<link href="node_modules/@wordpress/block-editor/build-style/style.css" rel="stylesheet" vite-ignore/> +``` +:::note + +There are more styles needed but can’t be added here because they are for the editor’s content which is in a separate document within an `<iframe>`. We’ll add those styles via the `BlockCanvas` component in a later step. + +::: + +Next, we’ll add blocks for the editor to work with. In `src/main.jsx` import and call `registerCoreBlocks`: +```js +import { registerCoreBlocks } from '@wordpress/block-library' +registerCoreBlocks(); +``` + +Finally, we’ll put our editor together. In `src/App.jsx` replace the contents with the following code: - - Update your `index.jsx` file with the following code: ```jsx -import { createElement, useState } from "react"; -import { createRoot } from 'react-dom/client'; +import { useState } from "react"; import { BlockEditorProvider, BlockCanvas, } from "@wordpress/block-editor"; -import { registerCoreBlocks } from "@wordpress/block-library"; - -// Default styles that are needed for the editor. -import "@wordpress/components/build-style/style.css"; -import "@wordpress/block-editor/build-style/style.css"; - -// Default styles that are needed for the core blocks. -import "@wordpress/block-library/build-style/common.css"; -import "@wordpress/block-library/build-style/style.css"; -import "@wordpress/block-library/build-style/editor.css"; - -// Register the default core block types. -registerCoreBlocks(); -function Editor() { - const [blocks, setBlocks] = useState([]); +// Base styles for the content within the block canvas iframe. +import componentsStyles from "@wordpress/components/build-style/style.css?raw"; +import blockEditorContentStyles from "@wordpress/block-editor/build-style/content.css?raw"; +import blocksStyles from "@wordpress/block-library/build-style/style.css?raw"; +import blocksEditorStyles from "@wordpress/block-library/build-style/editor.css?raw"; + +const contentStyles = [ + { css: componentsStyles }, + { css: blockEditorContentStyles }, + { css: blocksStyles }, + { css: blocksEditorStyles }, +]; + +export default function Editor() { + const [ blocks, setBlocks ] = useState( [] ); return ( /* The BlockEditorProvider is the wrapper of the block editor's state. All the UI elements of the block editor need to be rendered within this provider. */ <BlockEditorProvider - value={blocks} - onChange={setBlocks} - onInput={setBlocks} + value={ blocks } + onChange={ setBlocks } + onInput={ setBlocks } > - {/* - The BlockCanvas component renders the block list within an iframe - and wire up all the necessary events to make the block editor work. - */} - <BlockCanvas height="500px" /> + { /* + The BlockCanvas component renders the block list within an iframe + and wires up all the necessary events to make the block editor work. + */ } + <BlockCanvas height="500px" styles={ contentStyles } /> </BlockEditorProvider> ); } -// Render your React component instead -const root = createRoot(document.getElementById("app")); -root.render(<Editor />); ``` That's it! You now have a very basic block editor with several block types included by default: paragraphs, headings, lists, quotes, images... diff --git a/schemas/json/theme.json b/schemas/json/theme.json index a1f51ace920259..4eec377e3a94b9 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -922,6 +922,9 @@ "core/file": { "$ref": "#/definitions/settingsPropertiesComplete" }, + "core/footnotes": { + "$ref": "#/definitions/settingsPropertiesComplete" + }, "core/freeform": { "$ref": "#/definitions/settingsPropertiesComplete" }, @@ -1030,9 +1033,6 @@ "core/post-terms": { "$ref": "#/definitions/settingsPropertiesComplete" }, - "core/post-time-to-read": { - "$ref": "#/definitions/settingsPropertiesComplete" - }, "core/post-title": { "$ref": "#/definitions/settingsPropertiesComplete" }, @@ -1063,6 +1063,9 @@ "core/query-title": { "$ref": "#/definitions/settingsPropertiesComplete" }, + "core/query-total": { + "$ref": "#/definitions/settingsPropertiesComplete" + }, "core/quote": { "$ref": "#/definitions/settingsPropertiesComplete" }, @@ -1102,9 +1105,6 @@ "core/table": { "$ref": "#/definitions/settingsPropertiesComplete" }, - "core/table-of-contents": { - "$ref": "#/definitions/settingsPropertiesComplete" - }, "core/tag-cloud": { "$ref": "#/definitions/settingsPropertiesComplete" }, @@ -1902,6 +1902,9 @@ "core/file": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, + "core/footnotes": { + "$ref": "#/definitions/stylesPropertiesAndElementsComplete" + }, "core/freeform": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, @@ -2010,9 +2013,6 @@ "core/post-terms": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, - "core/post-time-to-read": { - "$ref": "#/definitions/stylesPropertiesAndElementsComplete" - }, "core/post-title": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, @@ -2043,6 +2043,9 @@ "core/query-title": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, + "core/query-total": { + "$ref": "#/definitions/stylesPropertiesAndElementsComplete" + }, "core/quote": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, @@ -2082,9 +2085,6 @@ "core/table": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, - "core/table-of-contents": { - "$ref": "#/definitions/stylesPropertiesAndElementsComplete" - }, "core/tag-cloud": { "$ref": "#/definitions/stylesPropertiesAndElementsComplete" }, @@ -2316,6 +2316,9 @@ "core/file": { "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" }, + "core/footnotes": { + "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" + }, "core/freeform": { "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" }, @@ -2424,9 +2427,6 @@ "core/post-terms": { "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" }, - "core/post-time-to-read": { - "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" - }, "core/post-title": { "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" }, @@ -2457,6 +2457,9 @@ "core/query-title": { "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" }, + "core/query-total": { + "$ref": "#/definitions/stylesPropertiesAndElementsComplete" + }, "core/quote": { "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" }, @@ -2496,9 +2499,6 @@ "core/table": { "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" }, - "core/table-of-contents": { - "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" - }, "core/tag-cloud": { "$ref": "#/definitions/stylesVariationBlockPropertiesComplete" }, diff --git a/schemas/json/wp-env.json b/schemas/json/wp-env.json index 8aa604ed41ed1f..5761fb3d877116 100644 --- a/schemas/json/wp-env.json +++ b/schemas/json/wp-env.json @@ -49,7 +49,7 @@ "default": [] }, "port": { - "description": "The primary port number to use for the installation. You'll access the instance through the port: 'http://localhost:8888'.", + "description": "The primary port number to use for the installation. You'll access the instance through the port: http://localhost:8888", "type": "integer", "default": 8888 }, @@ -66,6 +66,10 @@ "phpmyadminPort": { "description": "The port number to access phpMyAdmin.", "type": "integer" + }, + "multisite": { + "description": "Whether to set up a multisite installation.", + "type": "boolean" } } }, @@ -78,7 +82,8 @@ "port", "config", "mappings", - "phpmyadminPort" + "phpmyadminPort", + "multisite" ] } }, @@ -109,6 +114,11 @@ } }, "default": {} + }, + "testsPort": { + "description": "The port number for the test site. You'll access the instance through the port: http://localhost:8889", + "type": "integer", + "default": 8889 } } }, @@ -120,7 +130,7 @@ "$ref": "#/definitions/wpEnvPropertyNames" }, { - "enum": [ "$schema", "env" ] + "enum": [ "$schema", "env", "testsPort" ] } ] } diff --git a/storybook/decorators/with-max-width-wrapper.js b/storybook/decorators/with-max-width-wrapper.js index ff979b93f213bf..84fb73f20b68f7 100644 --- a/storybook/decorators/with-max-width-wrapper.js +++ b/storybook/decorators/with-max-width-wrapper.js @@ -3,15 +3,12 @@ */ import styled from '@emotion/styled'; -/** - * A Storybook decorator to wrap a story in a div applying a max width and - * padding. This can be used to simulate real world constraints on components - * such as being located within the WordPress editor sidebars. - */ - -const Wrapper = styled.div` - max-width: 248px; -`; +const maxWidthWrapperMap = { + none: 0, + 'wordpress-sidebar': 248, + 'small-container': 600, + 'large-container': 960, +}; const Indicator = styled.div` display: flex; @@ -27,14 +24,19 @@ const Indicator = styled.div` `; export const WithMaxWidthWrapper = ( Story, context ) => { - if ( context.globals.maxWidthWrapper === 'none' ) { + /** + * A Storybook decorator to wrap a story in a div applying a max width. + * This can be used to simulate real world constraints on components + * such as being located within the WordPress editor sidebars. + */ + const maxWidth = maxWidthWrapperMap[ context.globals.maxWidthWrapper ]; + if ( ! maxWidth ) { return <Story { ...context } />; } - return ( - <Wrapper> + <div style={ { maxWidth } }> <Story { ...context } /> - <Indicator>Max-Width Wrapper - 248px</Indicator> - </Wrapper> + <Indicator>{ `Max-Width Wrapper - ${ maxWidth }px` }</Indicator> + </div> ); }; diff --git a/storybook/main.js b/storybook/main.js index 5261fbbf4726ab..29f24c223ccdfe 100644 --- a/storybook/main.js +++ b/storybook/main.js @@ -54,6 +54,7 @@ module.exports = { '@storybook/addon-a11y', '@storybook/addon-toolbars', '@storybook/addon-actions', + '@storybook/addon-webpack5-compiler-babel', 'storybook-source-link', '@geometricpanda/storybook-addon-badges', ], @@ -61,8 +62,9 @@ module.exports = { name: '@storybook/react-webpack5', options: {}, }, - docs: { - autodocs: true, + docs: {}, + typescript: { + reactDocgen: 'react-docgen-typescript', }, webpackFinal: async ( config ) => { return { diff --git a/storybook/preview.js b/storybook/preview.js index a7c9aa0c085fc1..b74640d9bcfbcf 100644 --- a/storybook/preview.js +++ b/storybook/preview.js @@ -2,7 +2,7 @@ * External dependencies */ import { - ArgsTable, + Controls, Description, Primary, Stories, @@ -86,6 +86,8 @@ export const globalTypes = { items: [ { value: 'none', title: 'None' }, { value: 'wordpress-sidebar', title: 'WP Sidebar' }, + { value: 'small-container', title: 'Small container' }, + { value: 'large-container', title: 'Large container' }, ], }, }, @@ -106,6 +108,9 @@ export const parameters = { sort: 'requiredFirst', }, docs: { + controls: { + sort: 'requiredFirst', + }, // Flips the order of the description and the primary component story // so the component is always visible before the fold. page: () => ( @@ -114,8 +119,7 @@ export const parameters = { <Subtitle /> <Primary /> <Description /> - { /* `story="^"` enables Controls for the primary props table */ } - <ArgsTable story="^" /> + <Controls /> <Stories includePrimary={ false } /> </> ), @@ -156,3 +160,5 @@ export const parameters = { }, sourceLinkPrefix: 'https://github.com/WordPress/gutenberg/blob/trunk/', }; + +export const tags = [ 'autodocs' ]; diff --git a/storybook/sidebar.js b/storybook/sidebar.js index d8ff2ba777dd7d..ab438440e3b834 100644 --- a/storybook/sidebar.js +++ b/storybook/sidebar.js @@ -28,7 +28,9 @@ const Title = styled.span( { const Icons = styled.span( {} ); -const Icon = styled.span( {} ); +const Icon = styled.span( { + lineHeight: 1, +} ); /** * Fetches tags from the Storybook API, and returns Icon @@ -41,7 +43,7 @@ function useIcons( item ) { return useMemo( () => { let data = {}; - if ( item.isComponent && item.children?.length ) { + if ( item.type === 'component' && item.children?.length ) { data = api.getData( item.children[ 0 ] ) ?? {}; } diff --git a/storybook/stories/docs/inline-icon.js b/storybook/stories/docs/inline-icon.js index d7d1fafc28723d..b947dbd534a00d 100644 --- a/storybook/stories/docs/inline-icon.js +++ b/storybook/stories/docs/inline-icon.js @@ -2,11 +2,13 @@ * External dependencies */ import styled from '@emotion/styled'; -import { Icons } from '@storybook/components'; -const StyledIcons = styled( Icons )` +const IconWrapper = ( { icon, ...props } ) => { + const IconComponent = icon; + return <IconComponent aria-hidden { ...props } />; +}; + +export const InlineIcon = styled( IconWrapper )` display: inline-block !important; width: 14px; `; - -export const InlineIcon = ( props ) => <StyledIcons aria-hidden { ...props } />; diff --git a/storybook/stories/docs/introduction.mdx b/storybook/stories/docs/introduction.mdx index 731c570942f6b8..cff649d189bd42 100644 --- a/storybook/stories/docs/introduction.mdx +++ b/storybook/stories/docs/introduction.mdx @@ -1,4 +1,5 @@ import { Meta } from '@storybook/blocks'; +import { RepoIcon } from '@storybook/icons'; import { InlineIcon } from './inline-icon'; <Meta title="Docs/Introduction" name="page" /> @@ -28,7 +29,7 @@ The site shows the individual components in the sidebar and the Canvas on the ri To view the documentation for each component use the **Docs** menu item in the top toolbar. -To view the source code for the component and its stories on GitHub, click the <InlineIcon icon="repository" /> View Source Repository button in the top right corner. +To view the source code for the component and its stories on GitHub, click the <InlineIcon icon={ RepoIcon } /> View Source Repository button in the top right corner. To use it in your local development environment run the following command in the top level Gutenberg directory: diff --git a/storybook/stories/foundations/layout.mdx b/storybook/stories/foundations/layout.mdx index abc0c7c4f6947f..578f4e8b66e428 100644 --- a/storybook/stories/foundations/layout.mdx +++ b/storybook/stories/foundations/layout.mdx @@ -1,4 +1,4 @@ -import { Meta } from '@storybook/addon-docs/blocks'; +import { Meta } from '@storybook/blocks'; import areas from './static/areas.svg'; import pageLayoutExample1 from './static/page-layout-example-1.svg'; import pageLayoutExample2 from './static/page-layout-example-2.svg'; @@ -27,32 +27,34 @@ At the highest level admin pages are comprised of _areas_, that can be arranged Areas can be combined in different ways depending on the use case. Here are some examples. <table> - <tr> - <td style={{verticalAlign: 'top', width: '50%'}}> - #### Sidebar, Content Frame and Preview Frame - <img src={ pageLayoutExample1 } alt="Diagram illustrating an example of the 'Sidebar, Content Frame and Preview Frame' arrangement" width="100%" /> - - A demonstration of this arrangement can be found in the Styles section of the Site Editor, and in the Pages and Templates sections when List layout is selected. - </td> - <td style={{verticalAlign: 'top', width: '50%'}}> - #### Sidebar and Preview Frame - <img src={ pageLayoutExample2 } alt="Diagram illustrating an example of the 'Sidebar and Preview Frame' arrangement" width="100%" /> - - A demonstration of this arrangement can be found in the Design section. - </td> - </tr> - <tr> - <td style={{verticalAlign: 'top', width: '50%'}}> - #### Sidebar and Content Frame - <img src={ pageLayoutExample3 } alt="Diagram illustrating an example of the 'Sidebar and Content Frame' arrangement" width="100%" /> - - A demonstration of this arrangement can be found in the Patterns and Templates sections of the Site Editor, or in the Pages section when Table or Grid layout are selected. - </td> - <td style={{verticalAlign: 'top', width: '50%'}}> - #### Sidebar and multiple Content Frames - <img src={ pageLayoutExample4 } alt="Diagram illustrating an example of the 'Sidebar and multiple Content Frames' arrangement" width="100%" /> - - Multiple content frames can be utilised as required. - </td> - </tr> + <tbody> + <tr> + <td style={{verticalAlign: 'top', width: '50%'}}> + #### Sidebar, Content Frame and Preview Frame + <img src={ pageLayoutExample1 } alt="Diagram illustrating an example of the 'Sidebar, Content Frame and Preview Frame' arrangement" width="100%" /> + + A demonstration of this arrangement can be found in the Styles section of the Site Editor, and in the Pages and Templates sections when List layout is selected. + </td> + <td style={{verticalAlign: 'top', width: '50%'}}> + #### Sidebar and Preview Frame + <img src={ pageLayoutExample2 } alt="Diagram illustrating an example of the 'Sidebar and Preview Frame' arrangement" width="100%" /> + + A demonstration of this arrangement can be found in the Design section. + </td> + </tr> + <tr> + <td style={{verticalAlign: 'top', width: '50%'}}> + #### Sidebar and Content Frame + <img src={ pageLayoutExample3 } alt="Diagram illustrating an example of the 'Sidebar and Content Frame' arrangement" width="100%" /> + + A demonstration of this arrangement can be found in the Patterns and Templates sections of the Site Editor, or in the Pages section when Table or Grid layout are selected. + </td> + <td style={{verticalAlign: 'top', width: '50%'}}> + #### Sidebar and multiple Content Frames + <img src={ pageLayoutExample4 } alt="Diagram illustrating an example of the 'Sidebar and multiple Content Frames' arrangement" width="100%" /> + + Multiple content frames can be utilised as required. + </td> + </tr> + </tbody> </table> diff --git a/storybook/stories/playground/box/index.js b/storybook/stories/playground/box/index.js index cca522a90c1441..35656c7d6edc04 100644 --- a/storybook/stories/playground/box/index.js +++ b/storybook/stories/playground/box/index.js @@ -12,7 +12,7 @@ import { /** * Internal dependencies */ -import editorStyles from '../editor-styles'; +import { editorStyles } from '../editor-styles'; import './style.css'; export default function EditorBox() { diff --git a/storybook/stories/playground/with-undo-redo/index.js b/storybook/stories/playground/with-undo-redo/index.js index b5a6067cad24b7..0952543679ce06 100644 --- a/storybook/stories/playground/with-undo-redo/index.js +++ b/storybook/stories/playground/with-undo-redo/index.js @@ -15,7 +15,7 @@ import { undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; /** * Internal dependencies */ -import editorStyles from '../editor-styles'; +import { editorStyles } from '../editor-styles'; import './style.css'; export default function EditorWithUndoRedo() { diff --git a/storybook/stories/playground/zoom-out/index.js b/storybook/stories/playground/zoom-out/index.js index 8b72a831d710e8..c4d9a716c90694 100644 --- a/storybook/stories/playground/zoom-out/index.js +++ b/storybook/stories/playground/zoom-out/index.js @@ -16,7 +16,7 @@ import { parse } from '@wordpress/blocks'; /** * Internal dependencies */ -import editorStyles from '../editor-styles'; +import { editorStyles } from '../editor-styles'; // eslint-disable-next-line @wordpress/dependency-group import contentCss from '!!raw-loader!../../../../packages/block-editor/build-style/content.css'; import { pattern } from './pattern'; diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index f5410f2230372b..bb93f342f9bf8c 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -8,7 +8,7 @@ import { defineConfig, devices } from '@playwright/test'; /** * WordPress dependencies */ -const baseConfig = require( '@wordpress/scripts/config/playwright.config' ); +import baseConfig from '@wordpress/scripts/config/playwright.config.js'; const config = defineConfig( { ...baseConfig, diff --git a/test/e2e/specs/editor/blocks/buttons.spec.js b/test/e2e/specs/editor/blocks/buttons.spec.js index d6b0a0a15c4ea2..ad19af747238db 100644 --- a/test/e2e/specs/editor/blocks/buttons.spec.js +++ b/test/e2e/specs/editor/blocks/buttons.spec.js @@ -263,12 +263,14 @@ test.describe( 'Buttons', () => { await editor.insertBlock( { name: 'core/buttons' } ); await page.keyboard.type( 'Content' ); await editor.openDocumentSettingsSidebar(); - await page.click( - `role=region[name="Editor settings"i] >> role=tab[name="Settings"i]` - ); - await page.click( - 'role=group[name="Button width"i] >> role=button[name="25%"i]' - ); + await page + .getByRole( 'region', { name: 'Editor settings' } ) + .getByRole( 'tab', { name: 'Settings' } ) + .click(); + await page + .getByRole( 'radiogroup', { name: 'Width' } ) + .getByRole( 'radio', { name: '25%' } ) + .click(); // Check the content. const content = await editor.getEditedPostContent(); diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index d3cddd9c3a51cd..79cb01038da23c 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -424,9 +424,6 @@ test.describe( 'Image', () => { page, editor, } ) => { - // This is a temp workaround for dragging and dropping images from the inserter. - // This should be removed when we have the zoom out view for media categories. - await page.setViewportSize( { width: 1400, height: 800 } ); await editor.insertBlock( { name: 'core/image' } ); const imageBlock = editor.canvas.getByRole( 'document', { name: 'Block: Image', diff --git a/test/e2e/specs/editor/blocks/navigation.spec.js b/test/e2e/specs/editor/blocks/navigation.spec.js index 83e95a08c0f6a2..769e30c99dab36 100644 --- a/test/e2e/specs/editor/blocks/navigation.spec.js +++ b/test/e2e/specs/editor/blocks/navigation.spec.js @@ -276,7 +276,7 @@ test.describe( 'Navigation block', () => { await pageUtils.pressKeys( 'ArrowDown' ); // remove the child link - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); const submenuBlock2 = editor.canvas.getByRole( 'document', { name: 'Block: Submenu', @@ -494,7 +494,7 @@ test.describe( 'Navigation block', () => { await pageUtils.pressKeys( 'ArrowDown', { times: 4 } ); await navigation.checkLabelFocus( 'wordpress.org' ); // Delete the nav link - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); // Focus moved to sibling await navigation.checkLabelFocus( 'Dog' ); // Add a link back so we can delete the first submenu link and see if focus returns to the parent submenu item @@ -507,15 +507,15 @@ test.describe( 'Navigation block', () => { await pageUtils.pressKeys( 'ArrowUp', { times: 2 } ); await navigation.checkLabelFocus( 'Dog' ); // Delete the nav link - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); await pageUtils.pressKeys( 'ArrowDown' ); // Focus moved to parent submenu item await navigation.checkLabelFocus( 'example.com' ); // Deleting this should move focus to the sibling item - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); await navigation.checkLabelFocus( 'Cat' ); // Deleting with no more siblings should focus the navigation block again - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); await expect( navBlock ).toBeFocused(); // Wait until the nav block inserter is visible before we continue. await expect( navBlockInserter ).toBeVisible(); diff --git a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js index 32334bfc777f2a..318707e22f098d 100644 --- a/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js +++ b/test/e2e/specs/editor/various/block-bindings/post-meta.spec.js @@ -524,6 +524,47 @@ test.describe( 'Post Meta source', () => { previewPage.locator( '#connected-paragraph' ) ).toHaveText( 'new value' ); } ); + + test( 'should be possible to edit the value of the connected custom fields in the inspector control registered by the plugin', async ( { + editor, + page, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + anchor: 'connected-paragraph', + content: 'fallback content', + metadata: { + bindings: { + content: { + source: 'core/post-meta', + args: { + key: 'movie_field', + }, + }, + }, + }, + }, + } ); + const contentInput = page.getByRole( 'textbox', { + name: 'Content', + } ); + await expect( contentInput ).toHaveValue( + 'Movie field default value' + ); + await contentInput.fill( 'new value' ); + // Check that the paragraph content attribute didn't change. + const [ paragraphBlockObject ] = await editor.getBlocks(); + expect( paragraphBlockObject.attributes.content ).toBe( + 'fallback content' + ); + // Check the value of the custom field is being updated by visiting the frontend. + const previewPage = await editor.openPreviewPage(); + await expect( + previewPage.locator( '#connected-paragraph' ) + ).toHaveText( 'new value' ); + } ); + test( 'should be possible to connect movie fields through the attributes panel', async ( { editor, page, diff --git a/test/e2e/specs/editor/various/block-deletion.spec.js b/test/e2e/specs/editor/various/block-deletion.spec.js index 9346412c46bcb2..5e4ead97c986fd 100644 --- a/test/e2e/specs/editor/various/block-deletion.spec.js +++ b/test/e2e/specs/editor/various/block-deletion.spec.js @@ -134,7 +134,7 @@ test.describe( 'Block deletion', () => { ).toBeFocused(); // Remove the current paragraph via dedicated keyboard shortcut. - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); // Ensure the last block was removed. await expect.poll( editor.getBlocks ).toMatchObject( [ diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js index a8895d282fb956..b31fc9e2cd1402 100644 --- a/test/e2e/specs/editor/various/block-locking.spec.js +++ b/test/e2e/specs/editor/various/block-locking.spec.js @@ -133,7 +133,7 @@ test.describe( 'Block Locking', () => { ).toBeVisible(); await paragraph.click(); - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); await expect.poll( editor.getBlocks ).toMatchObject( [ { diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index 531faa8bea049d..6bf1689a400499 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -809,8 +809,8 @@ test.describe( 'List View', () => { // Delete remaining blocks. // Keyboard shortcut should also work. - await pageUtils.pressKeys( 'access+z' ); - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); + await pageUtils.pressKeys( 'shift+Backspace' ); await expect .poll( listViewUtils.getBlocksWithA11yAttributes, @@ -842,7 +842,7 @@ test.describe( 'List View', () => { { name: 'core/heading', selected: false }, ] ); - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); await expect .poll( listViewUtils.getBlocksWithA11yAttributes, @@ -865,7 +865,7 @@ test.describe( 'List View', () => { .getByRole( 'gridcell', { name: 'File' } ) .getByRole( 'link' ) .focus(); - for ( const keys of [ 'Delete', 'Backspace', 'access+z' ] ) { + for ( const keys of [ 'Delete', 'Backspace', 'shift+Backspace' ] ) { await pageUtils.pressKeys( keys ); await expect .poll( @@ -1133,7 +1133,7 @@ test.describe( 'List View', () => { optionsForFileMenu, 'Pressing Space should also open the menu dropdown' ).toBeVisible(); - await pageUtils.pressKeys( 'access+z' ); // Keyboard shortcut for Delete. + await pageUtils.pressKeys( 'shift+Backspace' ); // Keyboard shortcut for Delete. await expect .poll( listViewUtils.getBlocksWithA11yAttributes, @@ -1153,7 +1153,7 @@ test.describe( 'List View', () => { optionsForFileMenu.getByRole( 'menuitem', { name: 'Delete' } ), 'The delete menu item should be hidden for locked blocks' ).toBeHidden(); - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); await expect .poll( listViewUtils.getBlocksWithA11yAttributes, diff --git a/test/e2e/specs/editor/various/template-resolution.spec.js b/test/e2e/specs/editor/various/template-resolution.spec.js index 13503ddaf23d5b..82e336feff7334 100644 --- a/test/e2e/specs/editor/various/template-resolution.spec.js +++ b/test/e2e/specs/editor/various/template-resolution.spec.js @@ -55,12 +55,15 @@ test.describe( 'Template resolution', () => { status: 'publish', } ); await admin.editPost( newPage.id ); + await page.locator( 'role=button[name="Block Inserter"i]' ).click(); await editor.openDocumentSettingsSidebar(); await expect( page.getByRole( 'button', { name: 'Template options' } ) ).toHaveText( 'Single Entries' ); await updateSiteSettings( { requestUtils, pageId: newPage.id } ); await page.reload(); + await page.locator( 'role=button[name="Block Inserter"i]' ).click(); + await editor.openDocumentSettingsSidebar(); await expect( page.getByRole( 'button', { name: 'Template options' } ) ).toHaveText( 'Index' ); @@ -81,6 +84,7 @@ test.describe( 'Template resolution', () => { postType: 'page', canvas: 'edit', } ); + await page.locator( 'role=button[name="Block Inserter"i]' ).click(); await editor.openDocumentSettingsSidebar(); await expect( page.getByRole( 'button', { name: 'Template options' } ) diff --git a/test/e2e/specs/editor/various/write-design-mode.spec.js b/test/e2e/specs/editor/various/write-design-mode.spec.js index 2892b4aea89e91..fb3e231e6fff60 100644 --- a/test/e2e/specs/editor/various/write-design-mode.spec.js +++ b/test/e2e/specs/editor/various/write-design-mode.spec.js @@ -100,6 +100,17 @@ test.describe( 'Write/Design mode', () => { expect( await getSelectedBlock() ).toEqual( sectionClientId ); + // open the block toolbar more settings menu + await page.getByLabel( 'Block tools' ).getByLabel( 'Options' ).click(); + + // get the length of the options menu + const optionsMenu = page + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem' ); + + // we expect 3 items in the options menu + await expect( optionsMenu ).toHaveCount( 3 ); + // We should be able to select the paragraph block and write in it. await paragraph.click(); await page.keyboard.type( ' something' ); diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-1-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-1-chromium.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc0f7a6b1dd7010f6e452350e97ea302ddd9e1b GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#6OcT~)9nwW7>k44ofy`glX(f`h<Um=hG<Mo o{`3F8J#*fH7WcdmDRBk{R(odQu<O<vfGQX~UHx3vIVCg!0B!abTmS$7 literal 0 HcmV?d00001 diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-2-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-2-chromium.png new file mode 100644 index 0000000000000000000000000000000000000000..7339cccdb78f288a62408313716a9cc0aba689c3 GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#6OcT~)9nwW7>k44ofy`glX(f`NO`(AhG<Mo s{`3F8J#*fH4*yAwjhyS>bWCMqFn`P<Xx7sAAE=7K)78&qol`;+032Z&*Z=?k literal 0 HcmV?d00001 diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-3-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-3-chromium.png new file mode 100644 index 0000000000000000000000000000000000000000..97943030eb1e88ca5e59d5c671411c42c34d31da GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#6OcT~)9nwW7>k44ofy`glX(f`NO`(AhG<Mo s{`3F8J#*fH6Ak{;8ad}bIWd)u!R!f(VCdfPt3Xu@p00i_>zopr04R|gIsgCw literal 0 HcmV?d00001 diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-4-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-4-chromium.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c455784e8a425cb06f4d1c3e8397e552ca19c2 GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#6OcT~)9nwW7>k44ofy`glX(f`NPD_ChG<Mo t{`3F8J#*fH69+!<_nQdI?G)7EWiZ*qA~4fM^f*u%gQu&X%Q~loCIA>r8Danc literal 0 HcmV?d00001 diff --git a/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-5-chromium.png b/test/e2e/specs/interactivity/__snapshots__/Router-styles-should-support-relative-URLs-in-referenced-style-sheets-5-chromium.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c455784e8a425cb06f4d1c3e8397e552ca19c2 GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^AT~D#6OcT~)9nwW7>k44ofy`glX(f`NPD_ChG<Mo t{`3F8J#*fH69+!<_nQdI?G)7EWiZ*qA~4fM^f*u%gQu&X%Q~loCIA>r8Danc literal 0 HcmV?d00001 diff --git a/test/e2e/specs/interactivity/directive-each.spec.ts b/test/e2e/specs/interactivity/directive-each.spec.ts index 511b38e7ddbb8b..3c015e63fe4bc1 100644 --- a/test/e2e/specs/interactivity/directive-each.spec.ts +++ b/test/e2e/specs/interactivity/directive-each.spec.ts @@ -18,7 +18,7 @@ test.describe( 'data-wp-each', () => { await utils.deleteAllPosts(); } ); - test( 'should use `item` as the defaul item name in the context', async ( { + test( 'should use `item` as the default item name in the context', async ( { page, } ) => { const elements = page.getByTestId( 'letters' ).getByTestId( 'item' ); @@ -500,4 +500,37 @@ test.describe( 'data-wp-each', () => { await expect( element ).toHaveText( 'beta' ); await expect( callbackRunCount ).toHaveText( '1' ); } ); + + for ( const testId of [ + 'each-with-unset', + 'each-with-null', + 'each-with-undefined', + ] ) { + test( `does not error with non-iterable values: ${ testId }`, async ( { + page, + } ) => { + await expect( page.getByTestId( testId ) ).toBeEmpty(); + } ); + } + + for ( const [ testId, values ] of [ + [ 'each-with-array', [ 'an', 'array' ] ], + [ 'each-with-set', [ 'a', 'set' ] ], + [ 'each-with-string', [ 's', 't', 'r' ] ], + [ 'each-with-generator', [ 'a', 'generator' ] ], + + // TODO: Is there a problem with proxies here? + // [ 'each-with-iterator', [ 'implements', 'iterator' ] ], + ] as const ) { + test( `support different each iterable values: ${ testId }`, async ( { + page, + } ) => { + const element = page.getByTestId( testId ); + for ( const value of values ) { + await expect( + element.getByText( value, { exact: true } ) + ).toBeVisible(); + } + } ); + } } ); diff --git a/test/e2e/specs/interactivity/fixtures/index.ts b/test/e2e/specs/interactivity/fixtures/index.ts index 607221ffb1ec43..08a72d20ef5ff7 100644 --- a/test/e2e/specs/interactivity/fixtures/index.ts +++ b/test/e2e/specs/interactivity/fixtures/index.ts @@ -18,8 +18,8 @@ export const test = base.extend< Fixtures >( { async ( { requestUtils }, use ) => { await use( new InteractivityUtils( { requestUtils } ) ); }, - // @ts-ignore: The required type is 'test', but can be 'worker' too. See + // This is a hack, 'worker' is a valid value but the type is wrong. // https://playwright.dev/docs/test-fixtures#worker-scoped-fixtures - { scope: 'worker' }, + { scope: 'worker' as 'test' }, ], } ); diff --git a/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts b/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts index fd850a6e39fae2..74436673f10b79 100644 --- a/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts +++ b/test/e2e/specs/interactivity/fixtures/interactivity-utils.ts @@ -6,6 +6,30 @@ import type { RequestUtils } from '@wordpress/e2e-test-utils-playwright'; type AddPostWithBlockOptions = { alias?: string; attributes?: Record< string, any >; + innerBlocks?: Block[]; +}; + +type Block = [ + type: string, + attributes?: Record< string, any >, + innerBlocks?: Block[], +]; + +const generateBlockMarkup = ( [ + type, + attributes, + innerBlocks, +]: Block ): string => { + const typeAndAttributes = attributes + ? `${ type } ${ JSON.stringify( attributes ) }` + : type; + + if ( ! innerBlocks ) { + return `<!-- wp:${ typeAndAttributes } /-->`; + } + return `<!-- wp:${ typeAndAttributes } -->${ innerBlocks + .map( generateBlockMarkup ) + .join( '' ) }<!--/ wp:${ type } -->`; }; export default class InteractivityUtils { @@ -40,7 +64,7 @@ export default class InteractivityUtils { async addPostWithBlock( name: string, - { attributes, alias }: AddPostWithBlockOptions = {} + { attributes, alias, innerBlocks }: AddPostWithBlockOptions = {} ) { const block = attributes ? `${ name } ${ JSON.stringify( attributes ) }` @@ -50,8 +74,14 @@ export default class InteractivityUtils { alias = block; } + const content = generateBlockMarkup( [ + name, + attributes, + innerBlocks, + ] ); + const payload = { - content: `<!-- wp:${ block } /-->`, + content, status: 'publish' as 'publish', date_gmt: '2023-01-01T00:00:00', title: alias, diff --git a/test/e2e/specs/interactivity/router-styles.spec.ts b/test/e2e/specs/interactivity/router-styles.spec.ts new file mode 100644 index 00000000000000..7bc575af37816c --- /dev/null +++ b/test/e2e/specs/interactivity/router-styles.spec.ts @@ -0,0 +1,232 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +const COLOR_RED = 'rgb(255, 0, 0)'; +const COLOR_GREEN = 'rgb(0, 255, 0)'; +const COLOR_BLUE = 'rgb(0, 0, 255)'; +const COLOR_WRAPPER = 'rgb(160, 12, 60)'; + +test.describe( 'Router styles', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + await utils.activatePlugins(); + const red = await utils.addPostWithBlock( + 'test/router-styles-wrapper', + { + alias: 'red', + innerBlocks: [ [ 'test/router-styles-red' ] ], + } + ); + const green = await utils.addPostWithBlock( + 'test/router-styles-wrapper', + { + alias: 'green', + innerBlocks: [ [ 'test/router-styles-green' ] ], + } + ); + const blue = await utils.addPostWithBlock( + 'test/router-styles-wrapper', + { + alias: 'blue', + innerBlocks: [ [ 'test/router-styles-blue' ] ], + } + ); + + const all = await utils.addPostWithBlock( + 'test/router-styles-wrapper', + { + alias: 'all', + innerBlocks: [ + [ 'test/router-styles-red' ], + [ 'test/router-styles-green' ], + [ 'test/router-styles-blue' ], + ], + } + ); + + await utils.addPostWithBlock( 'test/router-styles-wrapper', { + alias: 'none', + attributes: { links: { red, green, blue, all } }, + } ); + } ); + + test.beforeEach( async ( { page, interactivityUtils: utils } ) => { + await page.goto( utils.getLink( 'none' ) ); + } ); + + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'should add and remove styles from style tags', async ( { + page, + } ) => { + const csn = page.getByTestId( 'client-side navigation' ); + const red = page.getByTestId( 'red' ); + const green = page.getByTestId( 'green' ); + const blue = page.getByTestId( 'blue' ); + const all = page.getByTestId( 'all' ); + + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_WRAPPER ); + + await page.getByTestId( 'link red' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_RED ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_RED ); + + await page.getByTestId( 'link green' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_GREEN ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_GREEN ); + + await page.getByTestId( 'link blue' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_BLUE ); + await expect( all ).toHaveCSS( 'color', COLOR_BLUE ); + + await page.getByTestId( 'link all' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_RED ); + await expect( green ).toHaveCSS( 'color', COLOR_GREEN ); + await expect( blue ).toHaveCSS( 'color', COLOR_BLUE ); + await expect( all ).toHaveCSS( 'color', COLOR_BLUE ); + } ); + + test( 'should add and remove styles from referenced style sheets', async ( { + page, + } ) => { + const csn = page.getByTestId( 'client-side navigation' ); + const red = page.getByTestId( 'red-from-link' ); + const green = page.getByTestId( 'green-from-link' ); + const blue = page.getByTestId( 'blue-from-link' ); + const all = page.getByTestId( 'all-from-link' ); + + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_WRAPPER ); + + await page.getByTestId( 'link red' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_RED ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_RED ); + + await page.getByTestId( 'link green' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_GREEN ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_GREEN ); + + await page.getByTestId( 'link blue' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_BLUE ); + await expect( all ).toHaveCSS( 'color', COLOR_BLUE ); + + await page.getByTestId( 'link all' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_RED ); + await expect( green ).toHaveCSS( 'color', COLOR_GREEN ); + await expect( blue ).toHaveCSS( 'color', COLOR_BLUE ); + await expect( all ).toHaveCSS( 'color', COLOR_BLUE ); + } ); + + test( 'should support relative URLs in referenced style sheets', async ( { + page, + } ) => { + const csn = page.getByTestId( 'client-side navigation' ); + const background = page.getByTestId( 'background-from-link' ); + + await expect( background ).toHaveScreenshot(); + + await page.getByTestId( 'link red' ).click(); + + await expect( csn ).toBeVisible(); + await expect( background ).toHaveScreenshot(); + + await page.getByTestId( 'link green' ).click(); + + await expect( csn ).toBeVisible(); + await expect( background ).toHaveScreenshot(); + + await page.getByTestId( 'link blue' ).click(); + + await expect( csn ).toBeVisible(); + await expect( background ).toHaveScreenshot(); + + await page.getByTestId( 'link all' ).click(); + + await expect( csn ).toBeVisible(); + await expect( background ).toHaveScreenshot(); + } ); + + test( 'should update style tags with modified content', async ( { + page, + } ) => { + const csn = page.getByTestId( 'client-side navigation' ); + const red = page.getByTestId( 'red-from-inline' ); + const green = page.getByTestId( 'green-from-inline' ); + const blue = page.getByTestId( 'blue-from-inline' ); + const all = page.getByTestId( 'all-from-inline' ); + + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_WRAPPER ); + + await page.getByTestId( 'link red' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_RED ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_RED ); + + await page.getByTestId( 'link green' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_GREEN ); + await expect( blue ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( all ).toHaveCSS( 'color', COLOR_GREEN ); + + await page.getByTestId( 'link blue' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( green ).toHaveCSS( 'color', COLOR_WRAPPER ); + await expect( blue ).toHaveCSS( 'color', COLOR_BLUE ); + await expect( all ).toHaveCSS( 'color', COLOR_BLUE ); + + await page.getByTestId( 'link all' ).click(); + + await expect( csn ).toBeVisible(); + await expect( red ).toHaveCSS( 'color', COLOR_RED ); + await expect( green ).toHaveCSS( 'color', COLOR_GREEN ); + await expect( blue ).toHaveCSS( 'color', COLOR_BLUE ); + await expect( all ).toHaveCSS( 'color', COLOR_BLUE ); + } ); +} ); diff --git a/test/e2e/specs/site-editor/block-style-variations.spec.js b/test/e2e/specs/site-editor/block-style-variations.spec.js index 03fc5398f4a0a5..1fa8972d34d6c8 100644 --- a/test/e2e/specs/site-editor/block-style-variations.spec.js +++ b/test/e2e/specs/site-editor/block-style-variations.spec.js @@ -317,9 +317,7 @@ async function draftNewPage( page ) { // Create a Group block with 2 nested Group blocks. async function addPageContent( editor, page ) { - const inserterButton = page.locator( - 'role=button[name="Block Inserter"i]' - ); + const inserterButton = page.locator( 'role=tab[name="Blocks"i]' ); await inserterButton.click(); await page.type( 'role=searchbox[name="Search"i]', 'Group' ); await page.click( diff --git a/test/e2e/specs/site-editor/homepage-settings.spec.js b/test/e2e/specs/site-editor/homepage-settings.spec.js index d53130af23ac8b..e80e14830364ce 100644 --- a/test/e2e/specs/site-editor/homepage-settings.spec.js +++ b/test/e2e/specs/site-editor/homepage-settings.spec.js @@ -10,6 +10,14 @@ test.describe( 'Homepage Settings via Editor', () => { title: 'Homepage', status: 'publish', } ); + await requestUtils.createPage( { + title: 'Sample page', + status: 'publish', + } ); + await requestUtils.createPage( { + title: 'Draft page', + status: 'draft', + } ); } ); test.beforeEach( async ( { admin, page } ) => { @@ -28,27 +36,30 @@ test.describe( 'Homepage Settings via Editor', () => { ] ); } ); - test( 'should show "Set as homepage" action on pages with `publish` status', async ( { + test( 'should not show "Set as homepage" and "Set as posts page" action on pages with `draft` status', async ( { page, } ) => { - const samplePage = page + const draftPage = page .getByRole( 'gridcell' ) - .getByLabel( 'Homepage' ); - const samplePageRow = page + .getByLabel( 'Draft page' ); + const draftPageRow = page .getByRole( 'row' ) - .filter( { has: samplePage } ); - await samplePageRow.hover(); - await samplePageRow + .filter( { has: draftPage } ); + await draftPageRow.hover(); + await draftPageRow .getByRole( 'button', { name: 'Actions', } ) .click(); await expect( page.getByRole( 'menuitem', { name: 'Set as homepage' } ) - ).toBeVisible(); + ).toBeHidden(); + await expect( + page.getByRole( 'menuitem', { name: 'Set as posts page' } ) + ).toBeHidden(); } ); - test( 'should not show "Set as homepage" action on current homepage', async ( { + test( 'should show correct homepage actions based on current homepage or posts page', async ( { page, } ) => { const samplePage = page @@ -68,5 +79,32 @@ test.describe( 'Homepage Settings via Editor', () => { await expect( page.getByRole( 'menuitem', { name: 'Set as homepage' } ) ).toBeHidden(); + await expect( + page.getByRole( 'menuitem', { name: 'Set as posts page' } ) + ).toBeHidden(); + + const samplePageTwo = page + .getByRole( 'gridcell' ) + .getByLabel( 'Sample page' ); + const samplePageTwoRow = page + .getByRole( 'row' ) + .filter( { has: samplePageTwo } ); + // eslint-disable-next-line playwright/no-force-option + await samplePageTwoRow.click( { force: true } ); + await samplePageTwoRow + .getByRole( 'button', { + name: 'Actions', + } ) + .click(); + await page + .getByRole( 'menuitem', { name: 'Set as posts page' } ) + .click(); + await page.getByRole( 'button', { name: 'Set posts page' } ).click(); + await expect( + page.getByRole( 'menuitem', { name: 'Set as homepage' } ) + ).toBeHidden(); + await expect( + page.getByRole( 'menuitem', { name: 'Set as posts page' } ) + ).toBeHidden(); } ); } ); diff --git a/test/e2e/specs/site-editor/page-list.spec.js b/test/e2e/specs/site-editor/page-list.spec.js index fa9cb86cd1d62e..120ded6a2b6d06 100644 --- a/test/e2e/specs/site-editor/page-list.spec.js +++ b/test/e2e/specs/site-editor/page-list.spec.js @@ -2,19 +2,27 @@ * WordPress dependencies */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +/** + * External dependencies + */ +const path = require( 'path' ); + +const createPages = async ( requestUtils ) => { + await requestUtils.createPage( { + title: 'Privacy Policy', + status: 'publish', + } ); + await requestUtils.createPage( { + title: 'Sample Page', + status: 'publish', + } ); +}; test.describe( 'Page List', () => { test.beforeAll( async ( { requestUtils } ) => { // Activate a theme with permissions to access the site editor. await requestUtils.activateTheme( 'emptytheme' ); - await requestUtils.createPage( { - title: 'Privacy Policy', - status: 'publish', - } ); - await requestUtils.createPage( { - title: 'Sample Page', - status: 'publish', - } ); + await createPages( requestUtils ); } ); test.afterAll( async ( { requestUtils } ) => { @@ -53,4 +61,356 @@ test.describe( 'Page List', () => { page.getByRole( 'searchbox', { name: 'Search' } ) ).toHaveValue( 'Privacy' ); } ); + + test.describe( 'Quick Edit Mode', () => { + const fields = { + featuredImage: { + performEdit: async ( page ) => { + const placeholder = page.getByRole( 'button', { + name: 'Choose an image…', + } ); + await placeholder.click(); + const mediaLibrary = page.getByRole( 'dialog' ); + const TEST_IMAGE_FILE_PATH = path.resolve( + __dirname, + '../../assets/10x10_e2e_test_image_z9T8jK.png' + ); + + const fileChooserPromise = + page.waitForEvent( 'filechooser' ); + await mediaLibrary.getByText( 'Select files' ).click(); + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles( TEST_IMAGE_FILE_PATH ); + await mediaLibrary + .locator( '.media-frame-toolbar' ) + .waitFor( { + state: 'hidden', + } ); + + await mediaLibrary + .getByRole( 'button', { name: 'Select', exact: true } ) + .click(); + }, + assertInitialState: async ( page ) => { + const el = page.getByText( 'Choose an image…' ); + const placeholder = page.getByRole( 'button', { + name: 'Choose an image…', + } ); + await expect( el ).toBeVisible(); + await expect( placeholder ).toBeVisible(); + }, + assertEditedState: async ( page ) => { + const placeholder = page.getByRole( 'button', { + name: 'Choose an image…', + } ); + await expect( placeholder ).toBeHidden(); + const img = page.locator( + '.fields-controls__featured-image-image' + ); + await expect( img ).toBeVisible(); + }, + }, + statusVisibility: { + performEdit: async ( page ) => { + const statusAndVisibility = page.getByLabel( + 'Status & Visibility' + ); + await statusAndVisibility.click(); + const options = [ + 'Published', + 'Draft', + 'Pending Review', + 'Private', + ]; + + for ( const option of options ) { + await page + .getByRole( 'radio', { name: option } ) + .click(); + await expect( statusAndVisibility ).toContainText( + option + ); + + if ( option !== 'Private' ) { + await page + .getByRole( 'checkbox', { + name: 'Password protected', + } ) + .check(); + } + } + }, + assertInitialState: async ( page ) => { + const statusAndVisibility = page.getByLabel( + 'Status & Visibility' + ); + await expect( statusAndVisibility ).toContainText( + 'Published' + ); + }, + assertEditedState: async ( page ) => { + const statusAndVisibility = page.getByLabel( + 'Status & Visibility' + ); + await expect( statusAndVisibility ).toContainText( + 'Private' + ); + }, + }, + author: { + assertInitialState: async ( page ) => { + const author = page.getByLabel( 'Author' ); + await expect( author ).toContainText( 'admin' ); + }, + performEdit: async ( page ) => { + const author = page.getByLabel( 'Author' ); + await author.click(); + const selectElement = page.locator( + 'select:has(option[value="1"])' + ); + await selectElement.selectOption( { value: '1' } ); + }, + assertEditedState: async () => {}, + }, + date: { + assertInitialState: async ( page ) => { + const dateEl = page.getByLabel( 'Edit Date' ); + const date = new Date(); + const yy = String( date.getFullYear() ); + + await expect( dateEl ).toContainText( yy ); + }, + performEdit: async ( page ) => { + const dateEl = page.getByLabel( 'Edit Date' ); + await dateEl.click(); + const date = new Date(); + const yy = Number( date.getFullYear() ); + const yyEl = page.locator( + `input[type="number"][value="${ yy }"]` + ); + + await yyEl.focus(); + await page.keyboard.press( 'ArrowUp' ); + }, + assertEditedState: async ( page ) => { + const date = new Date(); + const yy = Number( date.getFullYear() ); + const dateEl = page.getByLabel( 'Edit Date' ); + await expect( dateEl ).toContainText( String( yy + 1 ) ); + }, + }, + slug: { + assertInitialState: async ( page ) => { + const slug = page.getByLabel( 'Edit Slug' ); + await expect( slug ).toContainText( 'privacy-policy' ); + }, + performEdit: async ( page ) => { + const slug = page.getByLabel( 'Edit Slug' ); + await slug.click(); + await expect( + page.getByRole( 'link', { + name: 'http://localhost:8889/?', + } ) + ).toBeVisible(); + }, + assertEditedState: async () => {}, + }, + parent: { + assertInitialState: async ( page ) => { + const parent = page.getByLabel( 'Edit Parent' ); + await expect( parent ).toContainText( 'None' ); + }, + performEdit: async ( page ) => { + const parent = page.getByLabel( 'Edit Parent' ); + await parent.click(); + await page + .getByLabel( 'Parent', { exact: true } ) + .fill( 'Sample' ); + + await page + .getByRole( 'option', { name: 'Sample Page' } ) + .click(); + }, + assertEditedState: async ( page ) => { + const parent = page.getByLabel( 'Edit Parent' ); + await expect( parent ).toContainText( 'Sample Page' ); + }, + }, + // TODO: Wrap up this test once https://github.com/WordPress/gutenberg/issues/68173 is fixed + // template: { + // assertInitialState: async ( page ) => { + // const template = page.getByRole( 'button', { + // name: 'Single Entries', + // } ); + // await expect( template ).toContainText( 'Single Entries' ); + // }, + // edit: async ( page ) => { + // const template = page.getByRole( 'button', { + // name: 'Single Entries', + // } ); + // await template.click(); + // await page + // .getByRole( 'menuitem', { name: 'Swap template' } ) + // .click(); + // }, + // assertEditedState: async ( page ) => { + // + // }, + // }, + discussion: { + assertInitialState: async ( page ) => { + const discussion = page.getByLabel( 'Edit Discussion' ); + await expect( discussion ).toContainText( 'Closed' ); + }, + performEdit: async ( page ) => { + const discussion = page.getByLabel( 'Edit Discussion' ); + await discussion.click(); + await page + .getByLabel( 'Open', { + exact: true, + } ) + .check(); + }, + assertEditedState: async ( page ) => { + const discussion = page.getByLabel( 'Edit Discussion' ); + await expect( discussion ).toContainText( 'Open' ); + }, + }, + }; + + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.setGutenbergExperiments( [ + 'gutenberg-quick-edit-dataviews', + ] ); + } ); + + test.beforeEach( async ( { admin, page } ) => { + await admin.visitSiteEditor(); + await page.getByRole( 'button', { name: 'Pages' } ).click(); + await page.getByRole( 'button', { name: 'Layout' } ).click(); + await page.getByRole( 'menuitemradio', { name: 'Table' } ).click(); + const privacyPolicyCheckbox = page.getByRole( 'checkbox', { + name: 'Select Item: Privacy Policy', + } ); + + await privacyPolicyCheckbox.check(); + + await page.getByRole( 'button', { name: 'Details' } ).click(); + } ); + + Object.entries( fields ).forEach( + ( [ + key, + { performEdit, assertInitialState, assertEditedState }, + ] ) => { + // Asserts are done in the individual functions + // eslint-disable-next-line playwright/expect-expect + test( `should initialize, edit, and update ${ key } field correctly`, async ( { + page, + } ) => { + await assertInitialState( page ); + await performEdit( page ); + await assertEditedState( page ); + } ); + } + ); + + test( 'should save multiple field changes and update Data Views UI', async ( { + page, + requestUtils, + } ) => { + const selectedItem = page.locator( '.is-selected' ); + const imagePlaceholder = selectedItem.locator( + '.fields-controls__featured-image-placeholder' + ); + const status = selectedItem.getByRole( 'cell', { + name: 'Published', + } ); + await expect( status ).toBeVisible(); + + const { featuredImage, statusVisibility } = fields; + await statusVisibility.performEdit( page ); + await featuredImage.performEdit( page ); + // Ensure that no dropdown is open + await page.getByRole( 'button', { name: 'Close' } ).click(); + const saveButton = page.getByLabel( 'Review 1 change…' ); + await saveButton.click(); + await page.getByRole( 'button', { name: 'Save' } ).click(); + const updatedStatus = selectedItem.getByRole( 'cell', { + name: 'Private', + } ); + await expect( imagePlaceholder ).toBeHidden(); + await expect( updatedStatus ).toBeVisible(); + + // Reset the page to its original state + await requestUtils.deleteAllPages(); + await createPages( requestUtils ); + } ); + + // TODO: Wrap up this test once https://github.com/WordPress/gutenberg/pull/67584 is merged + // test( 'should update pages according to the changes', async ( { + // page, + // } ) => { + // const samplePage = page.getByRole( 'checkbox', { + // name: 'Select Item: Sample Page', + // } ); + + // await samplePage.check(); + + // const table = page.getByRole( 'table' ); + + // const selectedItems = table.locator( '.is-selected', { + // strict: false, + // } ); + + // expect( await selectedItems.all() ).toHaveLength( 2 ); + + // const imagePlaceholders = selectedItems.locator( + // '.fields-controls__featured-image-placeholder', + // { strict: false } + // ); + + // for ( const imagePlaceholder of await imagePlaceholders.all() ) { + // await expect( imagePlaceholder ).toBeVisible(); + // } + + // const statuses = selectedItems.getByRole( 'cell', { + // name: 'Public', + // } ); + + // for ( const status of await statuses.all() ) { + // await expect( status ).toBeVisible(); + // } + + // const { featuredImage, statusVisibility } = fields; + // await statusVisibility.edit( page ); + // await featuredImage.edit( page ); + // // Ensure that no dropdown is open + // await page.getByRole( 'button', { name: 'Close' } ).click(); + // const saveButton = page.getByLabel( 'Review 1 change…' ); + // await saveButton.click(); + // await page.getByRole( 'button', { name: 'Save' } ).click(); + // const updatedStatus = selectedItems.getByRole( + // 'cell', + // { + // name: 'Private', + // }, + // { + // strict: false, + // } + // ); + + // for ( const imagePlaceholder of await imagePlaceholders.all() ) { + // await expect( imagePlaceholder ).toBeHidden(); + // } + + // for ( const status of await updatedStatus.all() ) { + // await expect( status ).toBeVisible(); + // } + // } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.setGutenbergExperiments( [] ); + } ); + } ); } ); diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index 4817651bac8f9d..37b164e85a5973 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -272,6 +272,7 @@ test.describe( 'Pages', () => { // Create new page that has the default template so as to swap it. await draftNewPage( page ); + await page.locator( 'role=button[name="Block Inserter"i]' ).click(); await editor.openDocumentSettingsSidebar(); const templateOptionsButton = page .getByRole( 'region', { name: 'Editor settings' } ) @@ -294,6 +295,7 @@ test.describe( 'Pages', () => { } ); // Now reset, and apply the default template back. + await editor.openDocumentSettingsSidebar(); await templateOptionsButton.click(); const resetButton = page .getByRole( 'menu', { name: 'Template options' } ) @@ -308,6 +310,7 @@ test.describe( 'Pages', () => { editor, } ) => { await draftNewPage( page ); + await page.locator( 'role=button[name="Block Inserter"i]' ).click(); await editor.openDocumentSettingsSidebar(); const templateOptionsButton = page .getByRole( 'region', { name: 'Editor settings' } ) diff --git a/test/e2e/specs/site-editor/template-part.spec.js b/test/e2e/specs/site-editor/template-part.spec.js index d88273574bc4b0..9d5c0ca05b0d9c 100644 --- a/test/e2e/specs/site-editor/template-part.spec.js +++ b/test/e2e/specs/site-editor/template-part.spec.js @@ -375,7 +375,7 @@ test.describe( 'Template Part', () => { await editor.selectBlocks( siteTitle ); // Remove the default site title block. - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); // Insert a group block with a Site Title block inside. await editor.insertBlock( { diff --git a/test/e2e/specs/site-editor/template-registration.spec.js b/test/e2e/specs/site-editor/template-registration.spec.js index ed89c7d18bf3fb..2960367fc32ef1 100644 --- a/test/e2e/specs/site-editor/template-registration.spec.js +++ b/test/e2e/specs/site-editor/template-registration.spec.js @@ -100,8 +100,7 @@ test.describe( 'Block template registration', () => { page, } ) => { // Create a post. - await admin.visitAdminPage( '/post-new.php' ); - await page.getByLabel( 'Close', { exact: true } ).click(); + await admin.createNewPost(); await editor.insertBlock( { name: 'core/paragraph', attributes: { content: 'User-created post.' }, @@ -128,7 +127,7 @@ test.describe( 'Block template registration', () => { blockTemplateRegistrationUtils, } ) => { // Create a post. - await admin.visitAdminPage( '/post-new.php' ); + await admin.createNewPost(); await editor.insertBlock( { name: 'core/paragraph', attributes: { content: 'User-created post.' }, diff --git a/test/e2e/specs/site-editor/zoom-out.spec.js b/test/e2e/specs/site-editor/zoom-out.spec.js index 77d121e1999397..493b566671f8be 100644 --- a/test/e2e/specs/site-editor/zoom-out.spec.js +++ b/test/e2e/specs/site-editor/zoom-out.spec.js @@ -234,6 +234,39 @@ test.describe( 'Zoom Out', () => { await expect( fourthSectionStart ).not.toBeInViewport(); } ); + test( 'Zoom out selected section has three items in options menu', async ( { + page, + } ) => { + // open the inserter + await page + .getByRole( 'button', { + name: 'Block Inserter', + exact: true, + } ) + .click(); + // switch to patterns tab + await page.getByRole( 'tab', { name: 'Patterns' } ).click(); + // search for a pattern + await page + .getByRole( 'searchbox', { name: 'Search' } ) + .fill( 'Footer' ); + // click on Footer with colophon, 3 columns + await page + .getByRole( 'option', { name: 'Footer with colophon, 3 columns' } ) + .click(); + + // open the block toolbar more settings menu + await page.getByLabel( 'Block tools' ).getByLabel( 'Options' ).click(); + + // get the length of the options menu + const optionsMenu = page + .getByRole( 'menu', { name: 'Options' } ) + .getByRole( 'menuitem' ); + + // we expect 3 items in the options menu + await expect( optionsMenu ).toHaveCount( 3 ); + } ); + test( 'Zoom Out cannot be activated when the section root is missing', async ( { page, editor, diff --git a/test/e2e/specs/widgets/editing-widgets.spec.js b/test/e2e/specs/widgets/editing-widgets.spec.js index 019e07fe87daac..f4d160f8a36db3 100644 --- a/test/e2e/specs/widgets/editing-widgets.spec.js +++ b/test/e2e/specs/widgets/editing-widgets.spec.js @@ -573,7 +573,7 @@ test.describe( 'Widgets screen', () => { .getByRole( 'document', { name: 'Block: Paragraph' } ) .filter( { hasText: 'Second Paragraph' } ) .focus(); - await pageUtils.pressKeys( 'access+z' ); + await pageUtils.pressKeys( 'shift+Backspace' ); await widgetsScreen.saveWidgets(); await expect.poll( widgetsScreen.getWidgetAreaBlocks ).toMatchObject( { diff --git a/test/e2e/tsconfig.json b/test/e2e/tsconfig.json index 28d349fc19bef7..080d514f6f3634 100644 --- a/test/e2e/tsconfig.json +++ b/test/e2e/tsconfig.json @@ -2,11 +2,11 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { + "checkJs": false, "noEmit": true, - "emitDeclarationOnly": false, - "allowJs": true, - "checkJs": false + "rootDir": ".", + "types": [ "node" ] }, - "include": [ "**/*" ], + "include": [ "." ], "exclude": [] } diff --git a/test/integration/fixtures/blocks/core__button__deprecated-v10.serialized.html b/test/integration/fixtures/blocks/core__button__deprecated-v10.serialized.html index e4c7b89c794619..0bebe131629f29 100644 --- a/test/integration/fixtures/blocks/core__button__deprecated-v10.serialized.html +++ b/test/integration/fixtures/blocks/core__button__deprecated-v10.serialized.html @@ -1,3 +1,3 @@ <!-- wp:button {"fontFamily":"cambria-georgia"} --> -<div class="wp-block-button has-cambria-georgia-font-family"><a class="wp-block-button__link wp-element-button">My button</a></div> +<div class="wp-block-button"><a class="wp-block-button__link has-cambria-georgia-font-family wp-element-button">My button</a></div> <!-- /wp:button --> diff --git a/test/integration/fixtures/blocks/core__button__deprecated-v12.html b/test/integration/fixtures/blocks/core__button__deprecated-v12.html new file mode 100644 index 00000000000000..b62b6f0020569f --- /dev/null +++ b/test/integration/fixtures/blocks/core__button__deprecated-v12.html @@ -0,0 +1,15 @@ +<!-- wp:button {"fontSize":"xx-large"} --> +<div class="wp-block-button has-custom-font-size has-xx-large-font-size"><a class="wp-block-button__link wp-element-button">My button 1</a></div> +<!-- /wp:button --> + +<!-- wp:button {"style":{"typography":{"fontStyle":"normal","fontWeight":"800"}}} --> +<div class="wp-block-button" style="font-style:normal;font-weight:800"><a class="wp-block-button__link wp-element-button">My button 2</a></div> +<!-- /wp:button --> + +<!-- wp:button {"style":{"typography":{"letterSpacing":"39px"}}} --> +<div class="wp-block-button" style="letter-spacing:39px"><a class="wp-block-button__link wp-element-button">My button 3</a></div> +<!-- /wp:button --> + +<!-- wp:button {"tagName":"button","style":{"typography":{"letterSpacing":"39px"}}} --> +<div class="wp-block-button" style="letter-spacing:39px"><button type="button" class="wp-block-button__link wp-element-button">My button 4</button></div> +<!-- /wp:button --> diff --git a/test/integration/fixtures/blocks/core__button__deprecated-v12.json b/test/integration/fixtures/blocks/core__button__deprecated-v12.json new file mode 100644 index 00000000000000..2c204623dc252f --- /dev/null +++ b/test/integration/fixtures/blocks/core__button__deprecated-v12.json @@ -0,0 +1,59 @@ +[ + { + "name": "core/button", + "isValid": true, + "attributes": { + "tagName": "a", + "type": "button", + "text": "My button 1", + "fontSize": "xx-large" + }, + "innerBlocks": [] + }, + { + "name": "core/button", + "isValid": true, + "attributes": { + "tagName": "a", + "type": "button", + "text": "My button 2", + "style": { + "typography": { + "fontStyle": "normal", + "fontWeight": "800" + } + } + }, + "innerBlocks": [] + }, + { + "name": "core/button", + "isValid": true, + "attributes": { + "tagName": "a", + "type": "button", + "text": "My button 3", + "style": { + "typography": { + "letterSpacing": "39px" + } + } + }, + "innerBlocks": [] + }, + { + "name": "core/button", + "isValid": true, + "attributes": { + "tagName": "button", + "type": "button", + "text": "My button 4", + "style": { + "typography": { + "letterSpacing": "39px" + } + } + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__button__deprecated-v12.parsed.json b/test/integration/fixtures/blocks/core__button__deprecated-v12.parsed.json new file mode 100644 index 00000000000000..d631bc600e49ac --- /dev/null +++ b/test/integration/fixtures/blocks/core__button__deprecated-v12.parsed.json @@ -0,0 +1,81 @@ +[ + { + "blockName": "core/button", + "attrs": { + "fontSize": "xx-large" + }, + "innerBlocks": [], + "innerHTML": "\n<div class=\"wp-block-button has-custom-font-size has-xx-large-font-size\"><a class=\"wp-block-button__link wp-element-button\">My button 1</a></div>\n", + "innerContent": [ + "\n<div class=\"wp-block-button has-custom-font-size has-xx-large-font-size\"><a class=\"wp-block-button__link wp-element-button\">My button 1</a></div>\n" + ] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ "\n\n" ] + }, + { + "blockName": "core/button", + "attrs": { + "style": { + "typography": { + "fontStyle": "normal", + "fontWeight": "800" + } + } + }, + "innerBlocks": [], + "innerHTML": "\n<div class=\"wp-block-button\" style=\"font-style:normal;font-weight:800\"><a class=\"wp-block-button__link wp-element-button\">My button 2</a></div>\n", + "innerContent": [ + "\n<div class=\"wp-block-button\" style=\"font-style:normal;font-weight:800\"><a class=\"wp-block-button__link wp-element-button\">My button 2</a></div>\n" + ] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ "\n\n" ] + }, + { + "blockName": "core/button", + "attrs": { + "style": { + "typography": { + "letterSpacing": "39px" + } + } + }, + "innerBlocks": [], + "innerHTML": "\n<div class=\"wp-block-button\" style=\"letter-spacing:39px\"><a class=\"wp-block-button__link wp-element-button\">My button 3</a></div>\n", + "innerContent": [ + "\n<div class=\"wp-block-button\" style=\"letter-spacing:39px\"><a class=\"wp-block-button__link wp-element-button\">My button 3</a></div>\n" + ] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n\n", + "innerContent": [ "\n\n" ] + }, + { + "blockName": "core/button", + "attrs": { + "tagName": "button", + "style": { + "typography": { + "letterSpacing": "39px" + } + } + }, + "innerBlocks": [], + "innerHTML": "\n<div class=\"wp-block-button\" style=\"letter-spacing:39px\"><button type=\"button\" class=\"wp-block-button__link wp-element-button\">My button 4</button></div>\n", + "innerContent": [ + "\n<div class=\"wp-block-button\" style=\"letter-spacing:39px\"><button type=\"button\" class=\"wp-block-button__link wp-element-button\">My button 4</button></div>\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__button__deprecated-v12.serialized.html b/test/integration/fixtures/blocks/core__button__deprecated-v12.serialized.html new file mode 100644 index 00000000000000..8de25b59343b3f --- /dev/null +++ b/test/integration/fixtures/blocks/core__button__deprecated-v12.serialized.html @@ -0,0 +1,15 @@ +<!-- wp:button {"fontSize":"xx-large"} --> +<div class="wp-block-button"><a class="wp-block-button__link has-xx-large-font-size has-custom-font-size wp-element-button">My button 1</a></div> +<!-- /wp:button --> + +<!-- wp:button {"style":{"typography":{"fontStyle":"normal","fontWeight":"800"}}} --> +<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" style="font-style:normal;font-weight:800">My button 2</a></div> +<!-- /wp:button --> + +<!-- wp:button {"style":{"typography":{"letterSpacing":"39px"}}} --> +<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" style="letter-spacing:39px">My button 3</a></div> +<!-- /wp:button --> + +<!-- wp:button {"tagName":"button","style":{"typography":{"letterSpacing":"39px"}}} --> +<div class="wp-block-button"><button type="button" class="wp-block-button__link wp-element-button" style="letter-spacing:39px">My button 4</button></div> +<!-- /wp:button --> diff --git a/test/performance/playwright.config.ts b/test/performance/playwright.config.ts index fafca3a589122f..75e87c4d2d0f00 100644 --- a/test/performance/playwright.config.ts +++ b/test/performance/playwright.config.ts @@ -8,7 +8,7 @@ import { defineConfig } from '@playwright/test'; /** * WordPress dependencies */ -const baseConfig = require( '@wordpress/scripts/config/playwright.config' ); +import baseConfig from '@wordpress/scripts/config/playwright.config.js'; process.env.ASSETS_PATH = path.join( __dirname, 'assets' ); diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index c09cfe3c67b444..5a0c7f0e952116 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -395,7 +395,7 @@ test.describe( 'Site Editor Performance', () => { await requestUtils.activateTheme( 'twentytwentyfour' ); } ); - const perPage = 20; + const perPage = 9; test( 'Run the test', async ( { page, admin, requestUtils } ) => { await Promise.all( diff --git a/test/performance/tsconfig.json b/test/performance/tsconfig.json index 28d349fc19bef7..080d514f6f3634 100644 --- a/test/performance/tsconfig.json +++ b/test/performance/tsconfig.json @@ -2,11 +2,11 @@ "$schema": "https://json.schemastore.org/tsconfig.json", "extends": "../../tsconfig.base.json", "compilerOptions": { + "checkJs": false, "noEmit": true, - "emitDeclarationOnly": false, - "allowJs": true, - "checkJs": false + "rootDir": ".", + "types": [ "node" ] }, - "include": [ "**/*" ], + "include": [ "." ], "exclude": [] } diff --git a/test/storybook-playwright/storybook/main.js b/test/storybook-playwright/storybook/main.js index b80833ca725f96..f68f586f477200 100644 --- a/test/storybook-playwright/storybook/main.js +++ b/test/storybook-playwright/storybook/main.js @@ -5,7 +5,10 @@ const baseConfig = require( '../../../storybook/main' ); const config = { ...baseConfig, - addons: [ '@storybook/addon-toolbars' ], + addons: [ + '@storybook/addon-toolbars', + '@storybook/addon-webpack5-compiler-babel', + ], docs: undefined, staticDirs: undefined, stories: [ diff --git a/test/unit/config/global-mocks.js b/test/unit/config/global-mocks.js index 8db2c180fadf3a..ce64f03b514be8 100644 --- a/test/unit/config/global-mocks.js +++ b/test/unit/config/global-mocks.js @@ -3,7 +3,6 @@ */ import { TextDecoder, TextEncoder } from 'node:util'; import { Blob as BlobPolyfill, File as FilePolyfill } from 'node:buffer'; -import 'core-js/stable/structured-clone'; jest.mock( '@wordpress/compose', () => { return { @@ -50,6 +49,3 @@ if ( ! global.TextEncoder ) { // Override jsdom built-ins with native node implementation. global.Blob = BlobPolyfill; global.File = FilePolyfill; - -// Polyfill structuredClone for jsdom. -global.structuredClone = structuredClone; diff --git a/tools/webpack/blocks.js b/tools/webpack/blocks.js index c05318d5b060f3..0bf72c58ba5688 100644 --- a/tools/webpack/blocks.js +++ b/tools/webpack/blocks.js @@ -8,7 +8,7 @@ const { realpathSync } = require( 'fs' ); /** * WordPress dependencies */ -const { PhpFilePathsPlugin } = require( '@wordpress/scripts/utils' ); +const PhpFilePathsPlugin = require( '@wordpress/scripts/plugins/php-file-paths-plugin' ); /** * Internal dependencies diff --git a/tsconfig.base.json b/tsconfig.base.json index a766eedaeddcaa..38c6ac761aab64 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -31,8 +31,12 @@ "resolveJsonModule": true, "typeRoots": [ "./typings", "./node_modules/@types" ], - "types": [] + "types": [], + + "rootDir": "${configDir}/src", + "declarationDir": "${configDir}/build-types" }, + "include": [ "${configDir}/src" ], "exclude": [ "**/*.android.js", "**/*.ios.js", diff --git a/tsconfig.json b/tsconfig.json index 1010054ea512ea..55759b5015bfd2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -55,10 +55,13 @@ { "path": "packages/sync" }, { "path": "packages/token-list" }, { "path": "packages/undo-manager" }, + { "path": "packages/upload-media" }, { "path": "packages/url" }, { "path": "packages/vips" }, { "path": "packages/warning" }, - { "path": "packages/wordcount" } + { "path": "packages/wordcount" }, + { "path": "test/e2e" }, + { "path": "test/performance" } ], "files": [] }