From 45105829d6363907af584b0e4321939148a3b938 Mon Sep 17 00:00:00 2001
From: Gutenberg Repository Automation
Date: Wed, 21 Aug 2024 16:14:01 +0000
Subject: [PATCH] Merge changes published in the Gutenberg plugin
"release/19.1" branch
---
.eslintrc.js | 47 +-
.github/workflows/props-bot.yml | 2 +-
backport-changelog/6.6/7145.md | 3 +
backport-changelog/6.7/7125.md | 3 +
backport-changelog/6.7/7137.md | 2 +
backport-changelog/6.7/7179.md | 5 +
changelog.txt | 369 +++++++++
docs/contributors/code/release.md | 2 +-
docs/explanations/architecture/modularity.md | 2 +-
.../fundamentals/block-wrapper.md | 2 +-
.../fundamentals/static-dynamic-rendering.md | 2 +-
.../applying-styles-with-stylesheets.md | 11 +
docs/manifest.json | 30 +
docs/reference-guides/core-blocks.md | 2 +-
.../reference-guides/filters/block-filters.md | 8 +-
.../interactivity-api/README.md | 47 +-
.../interactivity-api/api-reference.md | 51 +-
.../interactivity-api/core-concepts/README.md | 9 +
.../core-concepts/server-side-rendering.md | 491 ++++++++++++
.../the-reactive-and-declarative-mindset.md | 300 +++++++
...l-state-local-context-and-derived-state.md | 740 ++++++++++++++++++
docs/reference-guides/slotfills/README.md | 9 +-
docs/toc.json | 13 +
gutenberg.php | 2 +-
lib/block-supports/background.php | 2 +-
lib/class-wp-theme-json-gutenberg.php | 70 +-
lib/client-assets.php | 6 +-
lib/compat/wordpress-6.7/block-templates.php | 41 +
...utenberg-rest-templates-controller-6-7.php | 203 +++++
.../class-wp-block-templates-registry.php | 256 ++++++
lib/compat/wordpress-6.7/compat.php | 114 +++
lib/compat/wordpress-6.7/rest-api.php | 100 +++
lib/compat/wordpress-6.7/script-modules.php | 104 +++
lib/experimental/script-modules.php | 93 ---
lib/experiments-page.php | 6 +-
lib/load.php | 8 +
package-lock.json | 218 +++---
package.json | 6 +-
packages/base-styles/_colors.scss | 4 +-
packages/base-styles/_variables.scss | 28 +-
.../downloadable-block-icon/style.scss | 2 +-
packages/block-editor/README.md | 23 +-
packages/block-editor/package.json | 2 +-
.../block-editor/src/autocompleters/block.js | 3 +-
.../src/components/block-list/content.scss | 18 +-
.../block-list/use-block-props/index.js | 4 +-
.../block-list/use-in-between-inserter.js | 22 +-
.../block-settings-menu/block-mode-toggle.js | 59 +-
.../test/block-mode-toggle.js | 49 +-
.../components/block-tools/insertion-point.js | 11 +
.../src/components/block-tools/style.scss | 2 +-
.../block-tools/use-show-block-tools.js | 5 +-
.../block-tools/zoom-out-mode-inserters.js | 8 +-
.../block-variation-transforms/style.scss | 2 +-
.../components/border-radius-control/index.js | 1 +
.../border-radius-control/style.scss | 10 -
.../convert-to-group-buttons/index.js | 2 +-
.../components/date-format-picker/index.js | 2 +
.../font-appearance-control/index.js | 3 +
.../src/components/font-family/README.md | 13 +-
.../src/components/font-family/index.js | 3 +
.../global-styles/background-panel.js | 185 +++--
.../get-global-styles-changes.js | 5 +-
.../src/components/global-styles/hooks.js | 5 -
.../src/components/global-styles/index.js | 1 -
.../src/components/global-styles/style.scss | 15 +-
.../test/theme-file-uri-utils.js | 41 -
.../test/use-global-styles-output.js | 24 +-
.../global-styles/theme-file-uri-utils.js | 18 -
.../global-styles/use-global-styles-output.js | 11 +-
.../src/components/global-styles/utils.js | 36 +-
.../src/components/grid/grid-visualizer.js | 29 +-
.../src/components/height-control/index.js | 1 +
.../src/components/iframe/index.js | 6 +-
.../components/image-editor/zoom-dropdown.js | 26 +-
.../inner-blocks/button-block-appender.js | 12 +-
.../inner-blocks/default-block-appender.js | 27 +-
.../src/components/inner-blocks/index.js | 19 +-
.../components/inner-blocks/with-client-id.js | 19 -
.../components/inserter-list-item/style.scss | 1 +
.../src/components/inserter/library.js | 2 +
.../src/components/inserter/menu.js | 59 +-
.../src/components/inserter/quick-inserter.js | 5 +-
.../src/components/inserter/style.scss | 2 +-
.../components/line-height-control/README.md | 7 +
.../components/line-height-control/index.js | 3 +
.../src/components/link-control/style.scss | 2 +-
.../src/components/media-placeholder/index.js | 19 +-
.../src/components/media-upload/README.md | 2 +
.../components/media-upload/index.native.js | 2 +
.../multi-selection-inspector/index.js | 17 +-
.../responsive-block-control/README.md | 1 +
.../responsive-block-control/test/index.js | 1 +
.../segmented-text-control/index.js | 63 --
.../segmented-text-control/style.scss | 15 -
.../input-controls/spacing-input-control.js | 12 +-
.../spacing-sizes-control/style.scss | 32 +-
.../text-alignment-control/index.js | 28 +-
.../text-decoration-control/index.js | 28 +-
.../stories/index.story.js | 4 -
.../text-transform-control/index.js | 28 +-
.../stories/index.story.js | 4 -
.../src/components/url-input/style.scss | 4 +-
.../url-popover/image-url-input-ui.js | 2 +
.../components/use-block-drop-zone/index.js | 24 +-
.../components/writing-mode-control/index.js | 28 +-
packages/block-editor/src/hooks/background.js | 48 +-
.../block-editor/src/hooks/block-bindings.js | 111 +--
.../src/hooks/block-bindings.scss | 2 +-
.../block-editor/src/hooks/test/background.js | 60 ++
.../block-editor/src/layouts/constrained.js | 1 +
packages/block-editor/src/layouts/flex.js | 2 +
packages/block-editor/src/layouts/grid.js | 3 +
packages/block-editor/src/private-apis.js | 2 +
.../src/store/private-selectors.js | 36 +
packages/block-editor/src/store/reducer.js | 7 +
.../src/store/test/private-selectors.js | 89 +++
packages/block-editor/src/style.scss | 1 -
.../block-editor/src/utils/block-bindings.js | 98 +++
.../src/utils/test/transform-styles.js | 49 ++
packages/block-library/src/archives/edit.js | 1 +
packages/block-library/src/audio/edit.js | 7 +-
.../block-library/src/avatar/user-control.js | 1 +
packages/block-library/src/button/edit.js | 1 +
.../src/comment-author-name/block.json | 15 +-
.../src/comment-author-name/style.scss | 4 +
.../src/comment-content/block.json | 15 +-
.../src/comment-content/style.scss | 5 +
.../block-library/src/comment-date/block.json | 15 +-
.../block-library/src/comment-date/style.scss | 4 +
.../comments-pagination-arrow-controls.js | 1 +
.../src/cover/edit/inspector-controls.js | 1 -
packages/block-library/src/editor.scss | 1 +
packages/block-library/src/embed/edit.js | 11 +-
packages/block-library/src/embed/editor.scss | 4 +
.../src/embed/embed-placeholder.js | 11 +-
.../block-library/src/embed/embed-preview.js | 40 +-
packages/block-library/src/file/block.json | 12 +
packages/block-library/src/file/inspector.js | 1 +
packages/block-library/src/form-input/edit.js | 1 +
packages/block-library/src/form/edit.js | 2 +
packages/block-library/src/gallery/edit.js | 14 +-
.../block-library/src/gallery/editor.scss | 2 +-
.../src/gallery/gallery.native.js | 3 +-
packages/block-library/src/gallery/style.scss | 40 +-
packages/block-library/src/group/edit.js | 2 +-
packages/block-library/src/image/image.js | 8 +-
.../block-library/src/image/transforms.js | 8 +-
.../block-library/src/latest-posts/edit.js | 77 +-
.../src/latest-posts/editor.scss | 17 +-
.../block-library/src/list-item/block.json | 9 +-
packages/block-library/src/list/block.json | 9 +
packages/block-library/src/media-text/edit.js | 1 -
packages/block-library/src/missing/edit.js | 30 +-
.../src/navigation-link/editor.scss | 3 -
.../src/navigation/edit/index.js | 6 +-
.../edit/navigation-menu-name-control.js | 1 +
.../navigation/edit/overlay-menu-preview.js | 2 +
.../block-library/src/navigation/editor.scss | 5 +
.../block-library/src/navigation/index.php | 37 +-
.../src/post-author-biography/block.json | 15 +-
.../src/post-author-biography/style.scss | 4 +
.../src/post-author-name/block.json | 15 +-
.../src/post-author-name/style.scss | 4 +
.../block-library/src/post-author/block.json | 13 +
.../block-library/src/post-author/edit.js | 125 +--
.../block-library/src/post-author/editor.scss | 7 +
.../block-library/src/post-author/style.scss | 1 +
.../block-library/src/post-comment/edit.js | 2 +
.../block-library/src/post-excerpt/edit.js | 1 +
.../post-featured-image/dimension-controls.js | 5 +
.../src/post-featured-image/edit.js | 1 +
.../src/post-navigation-link/edit.js | 8 +-
packages/block-library/src/post-terms/edit.js | 1 +
packages/block-library/src/post-title/edit.js | 1 +
.../block-library/src/preformatted/block.json | 12 +
.../src/query-pagination-numbers/edit.js | 1 +
.../query-pagination-arrow-controls.js | 1 +
.../block-library/src/query-title/block.json | 12 +
.../edit/inspector-controls/parent-control.js | 1 +
.../src/query/edit/query-content.js | 2 +-
packages/block-library/src/quote/block.json | 1 +
.../src/quote/test/transforms.native.js | 2 +-
.../block-library/src/quote/transforms.js | 13 +-
packages/block-library/src/search/edit.js | 18 +-
packages/block-library/src/search/editor.scss | 7 +-
.../block-library/src/social-link/edit.js | 2 +
packages/block-library/src/style.scss | 5 +
packages/block-library/src/table/edit.js | 9 +-
.../block-library/src/tag-cloud/block.json | 12 +
packages/block-library/src/tag-cloud/edit.js | 80 +-
.../block-library/src/tag-cloud/editor.scss | 12 +-
.../template-part/edit/advanced-controls.js | 2 +
.../src/template-part/editor.scss | 5 -
.../src/video/edit-common-settings.js | 1 +
packages/block-library/src/video/edit.js | 4 +-
.../block-library/src/video/tracks-editor.js | 3 +
.../block-library/src/video/transforms.js | 6 +-
packages/commands/src/components/style.scss | 2 +-
packages/components/CHANGELOG.md | 80 ++
packages/components/CONTRIBUTING.md | 219 ++++--
packages/components/package.json | 2 +-
.../src/alignment-matrix-control/cell.tsx | 6 +-
.../src/alignment-matrix-control/index.tsx | 9 +-
.../styles/alignment-matrix-control-styles.ts | 4 +-
.../styles/angle-picker-control-styles.tsx | 4 +-
.../components/src/base-control/README.md | 7 +-
.../components/src/base-control/index.tsx | 102 ++-
packages/components/src/base-control/types.ts | 7 +
.../border-box-control/README.md | 7 +
.../border-control/border-control/README.md | 7 +
.../components/src/border-control/styles.ts | 2 +-
.../components/src/button-group/style.scss | 4 +-
packages/components/src/button/README.md | 7 +
packages/components/src/button/style.scss | 4 +-
.../components/src/checkbox-control/index.tsx | 1 +
.../checkbox-control/stories/index.story.tsx | 1 +
.../src/checkbox-control/test/index.tsx | 9 +-
.../circular-option-picker-option.tsx | 14 +-
.../circular-option-picker.tsx | 2 +-
.../src/circular-option-picker/style.scss | 6 +-
.../src/circular-option-picker/types.ts | 5 +-
.../components/src/color-indicator/style.scss | 2 +-
.../components/src/color-palette/style.scss | 4 +-
.../components/src/color-palette/utils.ts | 33 +-
.../src/color-picker/input-with-slider.tsx | 1 +
.../components/src/combobox-control/README.md | 8 +
.../components/src/combobox-control/index.tsx | 1 +
.../combobox-control/stories/index.story.tsx | 7 +-
.../src/combobox-control/test/index.tsx | 6 +-
packages/components/src/composite/README.md | 325 ++++++++
packages/components/src/composite/context.ts | 14 +
.../components/src/composite/current/index.ts | 20 -
.../composite/current/stories/index.story.tsx | 86 --
packages/components/src/composite/index.ts | 7 -
packages/components/src/composite/index.tsx | 341 ++++++++
.../components/src/composite/legacy/index.tsx | 29 +-
.../src/composite/legacy/stories/utils.tsx | 19 +
.../src/composite/stories/index.story.tsx | 466 +++++++++++
.../composite/{current => }/stories/utils.tsx | 29 +-
packages/components/src/composite/types.ts | 298 +++++++
packages/components/src/composite/v2.ts | 4 -
.../src/custom-gradient-picker/style.scss | 4 +-
.../custom-select.tsx | 3 +-
.../src/custom-select-control/README.md | 7 +
.../src/custom-select-control/test/index.tsx | 125 +++
.../src/custom-select-control/types.ts | 6 +-
.../components/src/date-time/time/index.tsx | 38 +-
packages/components/src/date-time/types.ts | 12 +-
.../src/dimension-control/README.md | 17 +
.../src/dimension-control/index.tsx | 35 +-
.../dimension-control/stories/index.story.tsx | 1 +
.../test/__snapshots__/index.test.js.snap | 16 -
.../src/dimension-control/test/index.test.js | 6 +-
.../components/src/dimension-control/types.ts | 12 +-
packages/components/src/drop-zone/style.scss | 2 +-
.../components/src/dropdown-menu-v2/index.tsx | 8 +-
.../components/src/dropdown-menu-v2/styles.ts | 32 +-
.../src/focal-point-picker/README.md | 10 +-
.../src/focal-point-picker/controls.tsx | 4 +-
.../src/focal-point-picker/index.tsx | 3 +-
.../stories/index.story.tsx | 3 +
.../styles/focal-point-picker-style.ts | 2 +-
.../src/focal-point-picker/test/index.tsx | 6 +-
.../src/focal-point-picker/types.ts | 4 +-
.../components/src/font-size-picker/README.md | 7 +
.../components/src/form-file-upload/README.md | 8 +
.../components/src/form-file-upload/types.ts | 6 +
.../components/src/form-token-field/README.md | 1 +
packages/components/src/guide/style.scss | 1 -
packages/components/src/index.ts | 2 +-
.../components/src/input-control/README.md | 8 +
packages/components/src/item-group/styles.ts | 2 +-
.../src/mobile/utils/alignments.native.js | 1 +
packages/components/src/modal/style.scss | 6 +-
.../navigator-back-button/component.tsx | 2 +-
.../navigator/navigator-back-button/hook.ts | 16 +-
.../navigator/navigator-provider/README.md | 74 +-
.../navigator-provider/component.tsx | 20 +-
.../src/navigator/navigator-screen/README.md | 14 +-
.../navigator-to-parent-button/README.md | 2 +
.../navigator-to-parent-button/component.tsx | 57 +-
.../components/src/navigator/test/index.tsx | 142 +++-
packages/components/src/navigator/types.ts | 55 +-
.../components/src/number-control/README.md | 8 +
.../components/src/palette-edit/styles.ts | 10 +-
.../components/src/placeholder/style.scss | 5 +-
packages/components/src/popover/index.tsx | 12 +-
packages/components/src/popover/style.scss | 4 +-
.../components/src/popover/test/index.tsx | 34 +
packages/components/src/private-apis.ts | 18 +-
.../components/src/progress-bar/styles.ts | 4 +-
.../components/src/query-controls/README.md | 7 +
.../components/src/query-controls/index.tsx | 11 +-
.../components/src/query-controls/types.ts | 3 +-
.../components/src/radio-control/index.tsx | 48 +-
.../components/src/radio-control/style.scss | 15 +-
.../src/radio-control/test/index.tsx | 83 +-
packages/components/src/radio-group/radio.tsx | 3 +-
.../components/src/range-control/README.md | 7 +
.../components/src/range-control/index.tsx | 48 +-
.../src/range-control/stories/index.story.tsx | 7 +
.../styles/range-control-styles.ts | 4 +-
.../src/range-control/test/index.tsx | 68 +-
.../components/src/search-control/index.tsx | 11 +-
.../search-control/stories/index.story.tsx | 1 +
.../src/search-control/test/index.tsx | 1 +
.../components/src/select-control/README.md | 8 +
.../components/src/select-control/index.tsx | 1 +
.../select-control/stories/index.story.tsx | 13 +-
.../select-control/test/select-control.tsx | 18 +-
packages/components/src/snackbar/style.scss | 4 +-
packages/components/src/tab-panel/index.tsx | 5 +-
packages/components/src/tab-panel/style.scss | 2 +-
packages/components/src/tabs/index.tsx | 3 +-
packages/components/src/tabs/tablist.tsx | 10 +-
packages/components/src/tabs/tabpanel.tsx | 8 +-
.../components/src/text-control/README.md | 8 +
.../components/src/text-control/index.tsx | 1 +
.../src/text-control/stories/index.story.tsx | 4 +-
.../src/text-control/test/text-control.tsx | 6 +-
packages/components/src/text/styles.ts | 2 +-
.../components/src/textarea-control/index.tsx | 1 +
.../textarea-control/stories/index.story.tsx | 2 +
.../styles/textarea-control-styles.ts | 77 +-
.../components/src/toggle-control/index.tsx | 9 +
.../toggle-control/stories/index.story.tsx | 1 +
.../src/toggle-control/test/index.tsx | 8 +-
.../test/__snapshots__/index.tsx.snap | 32 +-
.../src/toggle-group-control/test/index.tsx | 6 +-
.../styles.ts | 4 +-
.../toggle-group-control/README.md | 7 +
.../toggle-group-control/as-radio-group.tsx | 3 +-
.../toggle-group-control/component.tsx | 1 +
.../toggle-group-control/styles.ts | 2 +-
.../components/src/toolbar/toolbar/style.scss | 4 +-
packages/components/src/tools-panel/styles.ts | 2 +-
packages/components/src/tooltip/index.tsx | 3 +-
packages/components/src/tooltip/style.scss | 2 +-
packages/components/src/tree-select/index.tsx | 21 +-
.../src/tree-select/stories/index.story.tsx | 1 +
.../components/src/unit-control/README.md | 7 +
.../styles/unit-control-styles.ts | 2 +-
.../components/src/utils/config-values.js | 7 +-
packages/components/src/utils/input/base.js | 30 -
packages/components/src/utils/input/index.js | 1 -
.../src/utils/input/input-control.js | 63 --
.../core-data/src/entity-types/wp-template.ts | 4 +
packages/dataviews/CHANGELOG.md | 15 +
packages/dataviews/README.md | 54 +-
packages/dataviews/package.json | 2 +-
.../dataform/stories/index.story.tsx | 45 +-
.../dataviews-bulk-actions-toolbar/style.scss | 2 +-
.../components/dataviews-filters/index.tsx | 3 +
.../dataviews-filters/search-widget.tsx | 9 +-
.../components/dataviews-filters/style.scss | 2 +-
.../components/dataviews-pagination/index.tsx | 51 +-
.../dataviews-pagination/style.scss | 13 +-
.../dataviews-view-config/index.tsx | 61 +-
.../src/components/dataviews/index.tsx | 26 +-
.../components/dataviews/stories/fixtures.js | 30 +-
.../dataviews/stories/index.story.js | 8 +-
.../src/components/dataviews/style.scss | 12 -
.../src/dataform-controls/datetime.tsx | 43 +
.../dataviews/src/dataform-controls/index.tsx | 61 ++
.../src/dataform-controls/integer.tsx | 38 +
.../dataviews/src/dataform-controls/radio.tsx | 42 +
.../src/dataform-controls/select.tsx | 52 ++
.../src/dataform-controls/style.scss | 4 +
.../dataviews/src/dataform-controls/text.tsx | 40 +
.../src/dataforms-layouts/panel/index.tsx | 8 +-
.../src/dataforms-layouts/regular/index.tsx | 8 +-
.../dataviews-layouts/grid/density-picker.tsx | 100 +--
.../src/dataviews-layouts/grid/index.tsx | 2 +-
.../src/dataviews-layouts/grid/style.scss | 4 -
.../src/dataviews-layouts/list/index.tsx | 8 +-
.../table/column-header-menu.tsx | 8 +-
.../dataviews/src/field-types/datetime.tsx | 28 +
packages/dataviews/src/field-types/index.tsx | 5 +
.../dataviews/src/field-types/integer.tsx | 73 +-
packages/dataviews/src/field-types/text.tsx | 72 +-
packages/dataviews/src/normalize-fields.ts | 18 +-
packages/dataviews/src/style.scss | 1 +
.../src/test/filter-and-sort-data-view.js | 28 +
packages/dataviews/src/types.ts | 86 +-
.../plugins/block-template-registration.php | 72 ++
.../directive-context/render.php | 21 +
.../components/layout/use-should-iframe.js | 7 +-
.../src/components/welcome-guide/default.js | 22 +-
.../edit-post/src/store/private-selectors.js | 9 +-
.../components/add-new-template/style.scss | 2 +-
.../src/components/editor/style.scss | 2 +-
.../global-styles/background-panel.js | 3 -
.../font-library-modal/installed-fonts.js | 10 +-
.../font-library-modal/style.scss | 2 +-
.../components/global-styles/screen-block.js | 6 +-
.../components/global-styles/screen-css.js | 4 +-
.../global-styles/shadows-edit-panel.js | 1 +
.../global-styles/size-control/index.js | 15 +-
.../src/components/page-patterns/index.js | 6 +-
.../components/page-patterns/use-patterns.js | 63 +-
.../src/components/post-edit/index.js | 110 +--
.../src/components/post-fields/index.js | 67 +-
.../src/components/post-list/index.js | 6 +-
.../sidebar-dataviews/add-new-view.js | 1 +
.../custom-dataviews-list.js | 1 +
.../push-changes-to-global-styles/index.js | 1 +
.../src/utils/is-template-removable.js | 6 +-
.../src/utils/is-template-revertable.js | 3 +-
.../src/components/error-boundary/style.scss | 2 +-
.../src/components/welcome-guide/index.js | 22 +-
packages/editor/src/bindings/post-meta.js | 4 +-
.../src/components/block-manager/style.scss | 4 +-
.../create-template-part-modal/index.js | 1 +
.../error-boundary/style.native.scss | 2 +-
.../src/components/error-boundary/style.scss | 2 +-
.../global-styles-provider/index.js | 19 +-
.../src/components/inserter-sidebar/index.js | 14 +-
.../src/components/page-attributes/parent.js | 22 +-
.../src/components/post-actions/actions.js | 621 +--------------
.../src/components/post-actions/index.js | 36 +-
.../src/components/post-card-panel/index.js | 55 +-
.../src/components/post-discussion/style.scss | 9 -
.../components/post-featured-image/index.js | 2 +
.../src/components/post-format/style.scss | 6 -
.../post-publish-panel/postpublish.js | 10 +-
.../test/__snapshots__/index.js.snap | 16 +-
.../editor/src/components/post-slug/index.js | 1 +
.../src/components/post-status/style.scss | 19 -
.../hierarchical-term-selector.js | 4 +-
.../editor/src/components/post-url/index.js | 24 +-
.../components/save-publish-panels/style.scss | 6 +-
.../src/components/sidebar/post-summary.js | 36 +-
.../src/components/site-discussion/style.scss | 14 -
.../template-content-panel/index.js | 5 +-
.../dataviews/actions/duplicate-pattern.tsx | 40 +
.../actions/duplicate-post.native.tsx | 3 +
.../src/dataviews/actions/duplicate-post.tsx | 174 ++++
.../actions/duplicate-template-part.tsx | 70 ++
.../src/dataviews/actions/rename-post.tsx | 146 ++++
.../dataviews/actions/reorder-page.native.tsx | 3 +
.../src/dataviews/actions/reorder-page.tsx | 125 +++
.../src/dataviews/actions/reset-post.tsx | 3 +-
.../editor/src/dataviews/actions/utils.ts | 18 +-
.../dataviews/actions/view-post-revisions.tsx | 47 ++
.../src/dataviews/actions/view-post.tsx | 30 +
packages/editor/src/dataviews/fields/index.ts | 26 +
.../src/dataviews/store/private-actions.ts | 43 +-
packages/editor/src/dataviews/types.ts | 53 +-
packages/editor/src/private-apis.js | 2 +
packages/editor/src/store/private-actions.js | 2 +-
.../src/store/utils/is-template-revertable.js | 3 +-
packages/env/lib/wordpress.js | 54 +-
packages/interactivity/CHANGELOG.md | 14 +
packages/interactivity/src/directives.tsx | 14 +-
.../components/pattern-overrides-controls.js | 47 +-
.../reusable-block-convert-button.js | 1 +
packages/scripts/package.json | 2 +-
packages/style-engine/CHANGELOG.md | 4 +
packages/style-engine/README.md | 18 +
packages/style-engine/src/index.ts | 3 +
.../src/styles/background/index.ts | 26 +-
.../style-engine/src/styles/test/utils.js | 54 ++
packages/style-engine/src/styles/utils.ts | 24 +-
packages/style-engine/src/test/utils.js | 62 --
packages/style-engine/src/types.ts | 2 +-
.../edit/widget-type-selector.js | 2 +
.../src/blocks/legacy-widget/editor.scss | 10 +-
.../src/blocks/widget-group/editor.scss | 2 +-
phpunit/block-supports/background-test.php | 17 +-
phpunit/block-template-test.php | 37 +
.../block-navigation-block-hooks-test.php | 12 -
...tenberg-rest-templates-controller-test.php | 119 +++
...class-wp-block-templates-registry-test.php | 192 +++++
phpunit/class-wp-theme-json-test.php | 173 +++-
test/e2e/specs/editor/blocks/image.spec.js | 56 +-
.../interactivity/directive-context.spec.ts | 15 +
test/e2e/specs/site-editor/patterns.spec.js | 7 -
.../site-editor/template-registration.spec.js | 361 +++++++++
.../.phpcs.xml.dist | 9 +-
.../ForbiddenFunctionsAndClassesSniff.php | 3 +-
.../GuardedFunctionAndClassNamesSniff.php | 3 +-
.../Sniffs/Commenting/SinceTagSniff.php | 122 ++-
.../ValidBlockLibraryFunctionNameSniff.php | 3 +-
.../Gutenberg/Tests/AbstractSniffUnitTest.php | 10 +-
.../ForbiddenFunctionsAndClassesUnitTest.php | 3 +-
.../GuardedFunctionAndClassNamesUnitTest.php | 3 +-
.../Tests/Commenting/SinceTagUnitTest.php | 3 +-
.../ValidBlockLibraryFunctionNameUnitTest.php | 3 +-
test/php/gutenberg-coding-standards/README.md | 8 +-
.../Tests/bootstrap.php | 87 +-
.../gutenberg-coding-standards/composer.json | 2 +-
492 files changed, 10865 insertions(+), 3644 deletions(-)
create mode 100644 backport-changelog/6.6/7145.md
create mode 100644 backport-changelog/6.7/7125.md
create mode 100644 backport-changelog/6.7/7179.md
create mode 100644 docs/reference-guides/interactivity-api/core-concepts/README.md
create mode 100644 docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md
create mode 100644 docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md
create mode 100644 docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md
create mode 100644 lib/compat/wordpress-6.7/block-templates.php
create mode 100644 lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php
create mode 100644 lib/compat/wordpress-6.7/class-wp-block-templates-registry.php
create mode 100644 lib/compat/wordpress-6.7/compat.php
create mode 100644 lib/compat/wordpress-6.7/rest-api.php
create mode 100644 lib/compat/wordpress-6.7/script-modules.php
delete mode 100644 packages/block-editor/src/components/global-styles/test/theme-file-uri-utils.js
delete mode 100644 packages/block-editor/src/components/global-styles/theme-file-uri-utils.js
delete mode 100644 packages/block-editor/src/components/inner-blocks/with-client-id.js
delete mode 100644 packages/block-editor/src/components/segmented-text-control/index.js
delete mode 100644 packages/block-editor/src/components/segmented-text-control/style.scss
create mode 100644 packages/block-editor/src/hooks/test/background.js
create mode 100644 packages/block-editor/src/utils/block-bindings.js
create mode 100644 packages/block-library/src/comment-author-name/style.scss
create mode 100644 packages/block-library/src/comment-date/style.scss
create mode 100644 packages/block-library/src/post-author-biography/style.scss
create mode 100644 packages/block-library/src/post-author-name/style.scss
create mode 100644 packages/block-library/src/post-author/editor.scss
create mode 100644 packages/components/src/composite/README.md
create mode 100644 packages/components/src/composite/context.ts
delete mode 100644 packages/components/src/composite/current/index.ts
delete mode 100644 packages/components/src/composite/current/stories/index.story.tsx
delete mode 100644 packages/components/src/composite/index.ts
create mode 100644 packages/components/src/composite/index.tsx
create mode 100644 packages/components/src/composite/stories/index.story.tsx
rename packages/components/src/composite/{current => }/stories/utils.tsx (62%)
create mode 100644 packages/components/src/composite/types.ts
delete mode 100644 packages/components/src/composite/v2.ts
delete mode 100644 packages/components/src/utils/input/base.js
delete mode 100644 packages/components/src/utils/input/index.js
delete mode 100644 packages/components/src/utils/input/input-control.js
create mode 100644 packages/dataviews/src/dataform-controls/datetime.tsx
create mode 100644 packages/dataviews/src/dataform-controls/index.tsx
create mode 100644 packages/dataviews/src/dataform-controls/integer.tsx
create mode 100644 packages/dataviews/src/dataform-controls/radio.tsx
create mode 100644 packages/dataviews/src/dataform-controls/select.tsx
create mode 100644 packages/dataviews/src/dataform-controls/style.scss
create mode 100644 packages/dataviews/src/dataform-controls/text.tsx
create mode 100644 packages/dataviews/src/field-types/datetime.tsx
create mode 100644 packages/e2e-tests/plugins/block-template-registration.php
create mode 100644 packages/editor/src/dataviews/actions/duplicate-pattern.tsx
create mode 100644 packages/editor/src/dataviews/actions/duplicate-post.native.tsx
create mode 100644 packages/editor/src/dataviews/actions/duplicate-post.tsx
create mode 100644 packages/editor/src/dataviews/actions/duplicate-template-part.tsx
create mode 100644 packages/editor/src/dataviews/actions/rename-post.tsx
create mode 100644 packages/editor/src/dataviews/actions/reorder-page.native.tsx
create mode 100644 packages/editor/src/dataviews/actions/reorder-page.tsx
create mode 100644 packages/editor/src/dataviews/actions/view-post-revisions.tsx
create mode 100644 packages/editor/src/dataviews/actions/view-post.tsx
create mode 100644 packages/editor/src/dataviews/fields/index.ts
create mode 100644 packages/style-engine/src/styles/test/utils.js
delete mode 100644 packages/style-engine/src/test/utils.js
create mode 100644 phpunit/block-template-test.php
create mode 100644 phpunit/class-gutenberg-rest-templates-controller-test.php
create mode 100644 phpunit/class-wp-block-templates-registry-test.php
create mode 100644 test/e2e/specs/site-editor/template-registration.spec.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 81408499bd34f4..13362e76c4e473 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -177,6 +177,8 @@ module.exports = {
'@wordpress/dependency-group': 'error',
'@wordpress/wp-global-usage': 'error',
'@wordpress/react-no-unsafe-timeout': 'error',
+ '@wordpress/i18n-hyphenated-range': 'error',
+ '@wordpress/i18n-no-flanking-whitespace': 'error',
'@wordpress/i18n-text-domain': [
'error',
{
@@ -278,10 +280,9 @@ module.exports = {
},
},
{
- // Temporary rules until we're ready to officially deprecate the bottom margins.
files: [ 'packages/*/src/**/*.[tj]s?(x)' ],
excludedFiles: [
- 'packages/components/src/**/@(test|stories)/**',
+ 'packages/*/src/**/@(test|stories)/**',
'**/*.@(native|ios|android).js',
],
rules: {
@@ -289,9 +290,12 @@ module.exports = {
'error',
...restrictedSyntax,
...restrictedSyntaxComponents,
+ // Temporary rules until we're ready to officially deprecate the bottom margins.
...[
+ 'BaseControl',
'CheckboxControl',
'ComboboxControl',
+ 'DimensionControl',
'FocalPointPicker',
'RangeControl',
'SearchControl',
@@ -307,6 +311,45 @@ module.exports = {
componentName +
' should have the `__nextHasNoMarginBottom` prop to opt-in to the new margin-free styles.',
} ) ),
+ // Temporary rules until we're ready to officially default to the new size.
+ ...[
+ 'BorderBoxControl',
+ 'BorderControl',
+ 'ComboboxControl',
+ 'CustomSelectControl',
+ 'DimensionControl',
+ 'FontAppearanceControl',
+ 'FontFamilyControl',
+ 'FontSizePicker',
+ 'FormTokenField',
+ 'InputControl',
+ 'LineHeightControl',
+ 'NumberControl',
+ 'RangeControl',
+ 'TextControl',
+ 'ToggleGroupControl',
+ ].map( ( componentName ) => ( {
+ // Falsy `__next40pxDefaultSize` without a non-default `size` prop.
+ selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"][value.expression.value!=false])):not(:has(JSXAttribute[name.name="size"][value.value!="default"]))`,
+ message:
+ componentName +
+ ' should have the `__next40pxDefaultSize` prop to opt-in to the new default size.',
+ } ) ),
+ {
+ // Falsy `__next40pxDefaultSize` without a `render` prop.
+ selector:
+ 'JSXOpeningElement[name.name="FormFileUpload"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"][value.expression.value!=false])):not(:has(JSXAttribute[name.name="render"]))',
+ message:
+ 'FormFileUpload should have the `__next40pxDefaultSize` prop to opt-in to the new default size.',
+ },
+ // Temporary rules until all existing components have the `__next40pxDefaultSize` prop.
+ ...[ 'SelectControl' ].map( ( componentName ) => ( {
+ // Not strict. Allows pre-existing __next40pxDefaultSize={ false } usage until they are all manually updated.
+ selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"])):not(:has(JSXAttribute[name.name="size"]))`,
+ message:
+ componentName +
+ ' should have the `__next40pxDefaultSize` prop to opt-in to the new default size.',
+ } ) ),
],
},
},
diff --git a/.github/workflows/props-bot.yml b/.github/workflows/props-bot.yml
index 0f21f47ef14f99..b2332aabb816c7 100644
--- a/.github/workflows/props-bot.yml
+++ b/.github/workflows/props-bot.yml
@@ -18,7 +18,7 @@ on:
# You cannot filter this event for PR comments only.
# However, the logic below does short-circuit the workflow for issues.
issue_comment:
- type:
+ types:
- created
# This event will run everytime a new PR review is initially submitted.
pull_request_review:
diff --git a/backport-changelog/6.6/7145.md b/backport-changelog/6.6/7145.md
new file mode 100644
index 00000000000000..386f765cb22fa8
--- /dev/null
+++ b/backport-changelog/6.6/7145.md
@@ -0,0 +1,3 @@
+https://github.com/WordPress/wordpress-develop/pull/7145
+
+* https://github.com/WordPress/gutenberg/pull/64076
diff --git a/backport-changelog/6.7/7125.md b/backport-changelog/6.7/7125.md
new file mode 100644
index 00000000000000..ce208decd2d145
--- /dev/null
+++ b/backport-changelog/6.7/7125.md
@@ -0,0 +1,3 @@
+https://github.com/WordPress/wordpress-develop/pull/7125
+
+* https://github.com/WordPress/gutenberg/pull/61577
diff --git a/backport-changelog/6.7/7137.md b/backport-changelog/6.7/7137.md
index 834cb29a21e6d9..00771b8bc6c21d 100644
--- a/backport-changelog/6.7/7137.md
+++ b/backport-changelog/6.7/7137.md
@@ -1,3 +1,5 @@
https://github.com/WordPress/wordpress-develop/pull/7137
+* https://github.com/WordPress/gutenberg/pull/64128
* https://github.com/WordPress/gutenberg/pull/64192
+* https://github.com/WordPress/gutenberg/pull/64328
diff --git a/backport-changelog/6.7/7179.md b/backport-changelog/6.7/7179.md
new file mode 100644
index 00000000000000..d777eace2cb05e
--- /dev/null
+++ b/backport-changelog/6.7/7179.md
@@ -0,0 +1,5 @@
+https://github.com/WordPress/wordpress-develop/pull/7179
+
+* https://github.com/WordPress/gutenberg/pull/64401
+* https://github.com/WordPress/gutenberg/pull/64459
+* https://github.com/WordPress/gutenberg/pull/64477
diff --git a/changelog.txt b/changelog.txt
index b7bbdf821f374e..748df8da3484c7 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,374 @@
== Changelog ==
+= 19.0.0 =
+
+## Changelog
+
+### Enhancements
+
+- Add alt edit field to the inline image in the format library ([64124](https://github.com/WordPress/gutenberg/pull/64124))
+- Update copy from "No Title" to "No title" across multiple places on the editor. ([64184](https://github.com/WordPress/gutenberg/pull/64184))
+- Update column input to be 40px by default. ([64190](https://github.com/WordPress/gutenberg/pull/64190))
+
+#### Block Library
+- Add anchor block support to List Items. ([48758](https://github.com/WordPress/gutenberg/pull/48758))
+- Unset the rowStart and columnStart attributes when a block inside the Grid is removed from a manual layout. ([64186](https://github.com/WordPress/gutenberg/pull/64186))
+- Update Group block example. ([63114](https://github.com/WordPress/gutenberg/pull/63114))
+- Make SiteLogoReplaceFlow always available in the Site Logo block toolbar. ([63499](https://github.com/WordPress/gutenberg/pull/63499))
+- Make Query Loop settings more intuitive with a ToggleGroup and simplified help text. ([63739](https://github.com/WordPress/gutenberg/pull/63739))
+- Move gallery link controls to the block toolbar. ([62762](https://github.com/WordPress/gutenberg/pull/62762))
+- Hide loading when the overlay menu is selected. ([64262](https://github.com/WordPress/gutenberg/pull/64262))
+- Move the Site Logo tooltip to the middle right. ([64296](https://github.com/WordPress/gutenberg/pull/64296))
+- Prevent duplicate spacing on Tag Cloud block. ([63832](https://github.com/WordPress/gutenberg/pull/63832))
+- Fix 'can user edit' Template Part check. ([64137](https://github.com/WordPress/gutenberg/pull/64137))
+- Add clearfix in Post content. ([63690](https://github.com/WordPress/gutenberg/pull/63690))
+- Tweak Tag Cloud controls and description. ([64151](https://github.com/WordPress/gutenberg/pull/64151))
+- Tweak list block. ([64025](https://github.com/WordPress/gutenberg/pull/64025))
+- Update MediaUpload button for the site logo from "Add media" to "Choose logo". ([63498](https://github.com/WordPress/gutenberg/pull/63498))
+- Update help text for sticky control in Query loop. ([63999](https://github.com/WordPress/gutenberg/pull/63999))
+- Add border support to the following blocks:
+ - [Time To Read](https://github.com/WordPress/gutenberg/pull/63776)
+ - [Categories List](https://github.com/WordPress/gutenberg/pull/63950)
+ - [Post Date](https://github.com/WordPress/gutenberg/pull/64023)
+ - [Post Excerpt](https://github.com/WordPress/gutenberg/pull/64022)
+ - [Post Terms](https://github.com/WordPress/gutenberg/pull/64246)
+ - [Post Title](https://github.com/WordPress/gutenberg/pull/64024)
+ - [Site Tagline](https://github.com/WordPress/gutenberg/pull/63778)
+ - [Site Title](https://github.com/WordPress/gutenberg/pull/63631)
+ - [Table of contents](https://github.com/WordPress/gutenberg/pull/63578)
+
+#### Extensibility
+- Add an async `__unstablePreSavePost` hook; resolving with false prevents saving. ([58022](https://github.com/WordPress/gutenberg/pull/58022))
+- Enable heading level curation. ([63535](https://github.com/WordPress/gutenberg/pull/63535))
+- Addition of `levelOptions` attribute to control available heading levels in [Post Title](https://github.com/WordPress/gutenberg/pull/64106), [Query Title](https://github.com/WordPress/gutenberg/pull/64107), [Site Tagline](https://github.com/WordPress/gutenberg/pull/64113), [Site Title](https://github.com/WordPress/gutenberg/pull/64111), and [Comments Title](https://github.com/WordPress/gutenberg/pull/64103).
+
+#### Data Views
+- Be more clear with the copy of the "hide" action. ([63047](https://github.com/WordPress/gutenberg/pull/63047))
+- Graduate data view options out of a menu to allow more design expression. ([64175](https://github.com/WordPress/gutenberg/pull/64175))
+- Move filter UI into a toggle-able panel to improve experience on narrow viewports/containers. ([63203](https://github.com/WordPress/gutenberg/pull/63203))
+- Update field line-height across grid/list layouts. ([63945](https://github.com/WordPress/gutenberg/pull/63945))
+- Update template description in table layout. ([63942](https://github.com/WordPress/gutenberg/pull/63942))
+- De-emphasise bulk actions on Grid layout. ([64209](https://github.com/WordPress/gutenberg/pull/64209))
+- Update the copy of some of the strings on dataviews actions. ([64099](https://github.com/WordPress/gutenberg/pull/64099))
+
+##### Dataviews Extensibility
+
+- Allow unregistering of the following post actions: [permanently delete](https://github.com/WordPress/gutenberg/pull/64088), [restore post](https://github.com/WordPress/gutenberg/pull/64134), and [trash post](https://github.com/WordPress/gutenberg/pull/64087).
+
+#### Dataform
+
+- Add author to quick edit page/post list. ([63983](https://github.com/WordPress/gutenberg/pull/63983))
+- If a field of type `text` declare `elements`, render it as a `SelectControl` in `edit`. ([64251](https://github.com/WordPress/gutenberg/pull/64251))
+- Migrate order action modal and introduce form validation. ([63895](https://github.com/WordPress/gutenberg/pull/63895))
+
+
+
+#### Components
+- Add radius scale. ([64007](https://github.com/WordPress/gutenberg/pull/64007))
+- Support generic props type on CustomSelectControl. ([63985](https://github.com/WordPress/gutenberg/pull/63985))
+- Guide: Add __next40pxDefaultSize to buttons. ([64181](https://github.com/WordPress/gutenberg/pull/64181))
+- Image: Make Placeholder white when there is a on top. ([63885](https://github.com/WordPress/gutenberg/pull/63885))
+- SelectControl: Infer `value` type from `options`. ([64069](https://github.com/WordPress/gutenberg/pull/64069))
+- SelectControl: Pass through `options` props. ([64211](https://github.com/WordPress/gutenberg/pull/64211))
+- TimeInput: Expose as subcomponent of TimePicker. ([63145](https://github.com/WordPress/gutenberg/pull/63145))
+- Update radius variables in components configuration. ([64133](https://github.com/WordPress/gutenberg/pull/64133))
+- `RadioControl`: Add support for option help text. ([63751](https://github.com/WordPress/gutenberg/pull/63751))
+
+#### Block Editor
+- Block Autocompleter: Force icon color to text color when item is selected. ([61376](https://github.com/WordPress/gutenberg/pull/61376))
+- Don't overlap canvas with inserter panel at large screens. ([64110](https://github.com/WordPress/gutenberg/pull/64110))
+- Format Library: Polish inline image format popover. ([64016](https://github.com/WordPress/gutenberg/pull/64016))
+- LineHeightControl: Hard deprecate bottom margin. ([64281](https://github.com/WordPress/gutenberg/pull/64281))
+- New useBlockElementRef hook for storing block element into a ref. ([63799](https://github.com/WordPress/gutenberg/pull/63799))
+- Improved tabbed sidebar styles. ([61974](https://github.com/WordPress/gutenberg/pull/61974))
+- URLInput: Hard deprecate bottom margin. ([64282](https://github.com/WordPress/gutenberg/pull/64282))
+
+#### Global Styles
+- Add a typesets section to Typography. ([62539](https://github.com/WordPress/gutenberg/pull/62539))
+- Add tooltips to the heading level selectors. ([64039](https://github.com/WordPress/gutenberg/pull/64039))
+- Background images: Ensure appropriate default values. ([64192](https://github.com/WordPress/gutenberg/pull/64192))
+- Create new public function to make it easier to expose style variations from other themes. ([63318](https://github.com/WordPress/gutenberg/pull/63318))
+- Style Book: Clearly denote heading levels. ([64038](https://github.com/WordPress/gutenberg/pull/64038))
+
+#### Design Tools
+- Column: Enable border radius support. ([63924](https://github.com/WordPress/gutenberg/pull/63924))
+- Comment Template: Add Border Block Support. ([64238](https://github.com/WordPress/gutenberg/pull/64238))
+- Post Comments Form: Add Border Block Support. ([64233](https://github.com/WordPress/gutenberg/pull/64233))
+
+#### Zoom Out
+- Add a control to enter and leave zoom out mode. ([63870](https://github.com/WordPress/gutenberg/pull/63870))
+- Improve zoom transition. ([64179](https://github.com/WordPress/gutenberg/pull/64179))
+
+#### Site Editor
+- Clarify that the site icon is a back button using an animation. ([63986](https://github.com/WordPress/gutenberg/pull/63986))
+- Site Icon: Add back filter effect to make it work for all kind of site icons. ([64172](https://github.com/WordPress/gutenberg/pull/64172))
+
+#### Post Editor
+- Tweak Create custom template modal. ([64255](https://github.com/WordPress/gutenberg/pull/64255))
+
+#### Icons
+- Add new "send" icon. ([64130](https://github.com/WordPress/gutenberg/pull/64130))
+
+#### Plugin
+- Bump minimum required WordPress version to 6.5. ([64126](https://github.com/WordPress/gutenberg/pull/64126))
+
+#### Font Library
+- Include a "Select All" options for google fonts. ([63893](https://github.com/WordPress/gutenberg/pull/63893))
+
+#### Block bindings
+- Allow bindings bootstrap after registration. ([63797](https://github.com/WordPress/gutenberg/pull/63797))
+
+#### Interactivity API
+- Refactor internal proxy and signals system. ([62734](https://github.com/WordPress/gutenberg/pull/62734))
+
+
+### New APIs
+
+- Make useStyleOverride public. ([63656](https://github.com/WordPress/gutenberg/pull/63656))
+
+
+### Bug Fixes
+
+- Core Data: Fix 'getEntityRecordPermissions' memoization. ([64091](https://github.com/WordPress/gutenberg/pull/64091))
+- Document bar: Fix long title with no spaces causing layout issue. ([64092](https://github.com/WordPress/gutenberg/pull/64092))
+- Fix density slider minus to be correct. ([64185](https://github.com/WordPress/gutenberg/pull/64185))
+- Fix: Deleting a pattern throws a notice saying undefined deleted. ([64301](https://github.com/WordPress/gutenberg/pull/64301))
+- Primitives: Add missing peer dependency. ([64218](https://github.com/WordPress/gutenberg/pull/64218))
+- Site Icon: Fix position in distraction free mode. ([64261](https://github.com/WordPress/gutenberg/pull/64261))
+
+#### Data Views
+- Add context to trash string. ([64249](https://github.com/WordPress/gutenberg/pull/64249))
+- Conditionally shows the description field in Template Grid layout. ([64043](https://github.com/WordPress/gutenberg/pull/64043))
+- Consider layout URL parameter when loading a default/custom view. ([64306](https://github.com/WordPress/gutenberg/pull/64306))
+- Display published date for pages/posts with published status. ([64049](https://github.com/WordPress/gutenberg/pull/64049))
+- Sort author by name + allow custom sort function. ([64064](https://github.com/WordPress/gutenberg/pull/64064))
+- Don't render action modal when there are no eligible items. ([64250](https://github.com/WordPress/gutenberg/pull/64250))
+- Pages: Update `useView` logic. ([63889](https://github.com/WordPress/gutenberg/pull/63889))
+- Update template preview dimensions in table layout. ([63938](https://github.com/WordPress/gutenberg/pull/63938))
+- Update template preview dimensions in table layout. ([63938](https://github.com/WordPress/gutenberg/pull/63938))
+
+#### Dataform
+
+- Fix SelectControl size and spacing. ([64324](https://github.com/WordPress/gutenberg/pull/64324))
+- Provide a better default for render when field has elements. ([64338](https://github.com/WordPress/gutenberg/pull/64338))
+
+#### Components
+- Autocompleter UI: Fix text color when hovering selected item. ([64294](https://github.com/WordPress/gutenberg/pull/64294))
+- BaseControl: change label's display: Block. ([63911](https://github.com/WordPress/gutenberg/pull/63911))
+- Button: Fix tertiary destructive hover style. ([64152](https://github.com/WordPress/gutenberg/pull/64152))
+- ColorPalette: Remove extra bottom margin when `CircularOptionPicker` is unneeded. ([63961](https://github.com/WordPress/gutenberg/pull/63961))
+- DropdownMenuV2: Break menu item help text on multiple lines for better truncation. ([63916](https://github.com/WordPress/gutenberg/pull/63916))
+- Fix modal dismissers in development mode. ([64132](https://github.com/WordPress/gutenberg/pull/64132))
+- Fix toggle help indentation. ([63903](https://github.com/WordPress/gutenberg/pull/63903))
+- Update the TextControl padding to match the rest of the controls. ([64326](https://github.com/WordPress/gutenberg/pull/64326))
+
+#### Global Styles
+- Fix block custom CSS pseudo element selectors. ([63980](https://github.com/WordPress/gutenberg/pull/63980))
+- Fix block library and global styles stylesheet ordering when a block style variation is active. ([63918](https://github.com/WordPress/gutenberg/pull/63918))
+- Style Book: Fix critical error when heading block is not registered. ([64047](https://github.com/WordPress/gutenberg/pull/64047))
+- TypesetButton: Check if variations exist before running logic. ([64139](https://github.com/WordPress/gutenberg/pull/64139))
+
+#### Site Editor
+- Centrally align entity in focused edit mode. ([64143](https://github.com/WordPress/gutenberg/pull/64143))
+- Don't trigger template ID resolution for multi-selected posts. ([64254](https://github.com/WordPress/gutenberg/pull/64254))
+- Long slugs breaking summary panel UI. ([64053](https://github.com/WordPress/gutenberg/pull/64053))
+
+#### Zoom Out
+- Keep top-level block selection if entering zoom out mode. ([64178](https://github.com/WordPress/gutenberg/pull/64178))
+- Use the block editor for insertion point data. ([63934](https://github.com/WordPress/gutenberg/pull/63934))
+
+#### Block Library
+- Fix a typo in use-image-sizes.js. ([64100](https://github.com/WordPress/gutenberg/pull/64100))
+- Template Part: Fix capability checks for inner blocks. ([64159](https://github.com/WordPress/gutenberg/pull/64159))
+- Update useTaxonomies hook to check for taxonomies for passed post type. ([64145](https://github.com/WordPress/gutenberg/pull/64145))
+
+#### Design Tools
+- Quote: Prevent block theme styles overriding global border and padding. ([64045](https://github.com/WordPress/gutenberg/pull/64045))
+- Spacing controls: Using CustomSelectControlV2 for >= 8 spacing sizes. ([64284](https://github.com/WordPress/gutenberg/pull/64284))
+
+#### Post Editor
+- Avoid errors for post types without a 'menu_icon'. ([64015](https://github.com/WordPress/gutenberg/pull/64015))
+- Post: Add a max length to the post password protected field. ([64156](https://github.com/WordPress/gutenberg/pull/64156))
+
+#### Grid layout
+- Fix grid resizer drag over embed. ([64098](https://github.com/WordPress/gutenberg/pull/64098))
+- Move resizer popover slot to fix display on mobile. ([63920](https://github.com/WordPress/gutenberg/pull/63920))
+
+#### Block Editor
+- Fix unexpected drag & rrop row/gallery creation logic. ([64241](https://github.com/WordPress/gutenberg/pull/64241))
+
+#### Icons
+- Remove hardcoded color from sidesAxial and sidesBottom icons. ([64174](https://github.com/WordPress/gutenberg/pull/64174))
+
+#### Document Settings
+- Display empty option when post author is missing. ([64165](https://github.com/WordPress/gutenberg/pull/64165))
+
+#### Patterns
+- Enable cross-browser support for pattern uploading. ([64123](https://github.com/WordPress/gutenberg/pull/64123))
+
+#### Commands
+- Fix 'Preferences' and 'Shortcuts' commands in StrictMode. ([64019](https://github.com/WordPress/gutenberg/pull/64019))
+
+#### Meta Boxes
+- Prevent popover from being hidden by metaboxes. ([63939](https://github.com/WordPress/gutenberg/pull/63939))
+
+#### Page Content Focus
+- TemplateContentPanel: Don't show content blocks that are in a Query Loop. ([63732](https://github.com/WordPress/gutenberg/pull/63732))
+
+#### Font Library
+- Fix item font family item height in the sidebar. ([63125](https://github.com/WordPress/gutenberg/pull/63125))
+
+#### Block API
+- Block categories - ensure that categories are unique by slug. ([62954](https://github.com/WordPress/gutenberg/pull/62954))
+
+
+### Accessibility
+
+- Restore focus style in dataviews grid view. ([64298](https://github.com/WordPress/gutenberg/pull/64298))
+- A11y text for site editor. ([62648](https://github.com/WordPress/gutenberg/pull/62648))
+- Accessibility issue of device preview options. ([63958](https://github.com/WordPress/gutenberg/pull/63958))
+
+#### Components
+- Improve the aria-disabled focus style of the Button. ([62480](https://github.com/WordPress/gutenberg/pull/62480))
+- Restore `describedBy` functionality on CustomSelectControl. ([63957](https://github.com/WordPress/gutenberg/pull/63957))
+
+#### Block Library
+- Fix unlabeled Spacer block controls. ([63806](https://github.com/WordPress/gutenberg/pull/63806))
+- Move Posts Per Page, Offset, and Pages controls from the block toolbar into Inspector Controls. ([58207](https://github.com/WordPress/gutenberg/pull/58207))
+
+#### Font Library
+- Remove notice context and add message when fonts are updated. ([64030](https://github.com/WordPress/gutenberg/pull/64030))
+
+
+### Performance
+
+- Add User Timings for the Interactivity API. ([60522](https://github.com/WordPress/gutenberg/pull/60522))
+
+#### Data Views
+- Optimize the patterns dataviews by extracting the fields definition. ([63927](https://github.com/WordPress/gutenberg/pull/63927))
+
+#### Layout
+- Avoid iterating auto grid inner blocks unless mode specifically changed. ([64194](https://github.com/WordPress/gutenberg/pull/64194))
+
+#### Block bindings
+- Move logic to merge `usesContext` outside the reducer. ([63941](https://github.com/WordPress/gutenberg/pull/63941))
+
+
+### Experiments
+
+- Adds experimental blocks flag. ([64121](https://github.com/WordPress/gutenberg/pull/64121))
+
+#### DataForm
+- Support multiple layouts and introduce the panel layout. ([64299](https://github.com/WordPress/gutenberg/pull/64299))
+
+#### DataViews Extensibility
+- Add a hook to allow third-party scripts to register/unregister post type actions. ([64138](https://github.com/WordPress/gutenberg/pull/64138))
+
+#### Grid Interactivity
+- Fix block mover layout and styles. ([64021](https://github.com/WordPress/gutenberg/pull/64021))
+
+#### Block bindings
+- UI for connecting bindings. ([62880](https://github.com/WordPress/gutenberg/pull/62880))
+
+
+### Documentation
+
+- .wp-env.json schema: Fix schema and add unit tests. ([63281](https://github.com/WordPress/gutenberg/pull/63281))
+- Add WP Studio to list of tools in documentation. ([64327](https://github.com/WordPress/gutenberg/pull/64327))
+- Add documentation for some dynamically generated selectors in the core-data store. ([64269](https://github.com/WordPress/gutenberg/pull/64269))
+- Block Editor: Update 'getBlocksByName' JSDoc. ([63919](https://github.com/WordPress/gutenberg/pull/63919))
+- Components: Add missing `__nextHasNoMarginBottom` documentation. ([64313](https://github.com/WordPress/gutenberg/pull/64313))
+- Corrected @deprecated doc Order in Inline Documentation. ([64013](https://github.com/WordPress/gutenberg/pull/64013))
+- Add documentation for `render_block` and `register_block_type_args` to Block Filters. ([64118](https://github.com/WordPress/gutenberg/pull/64118))
+- Fix interactivity API documentation link. ([64060](https://github.com/WordPress/gutenberg/pull/64060))
+- Fix non working link to an interactivity API example block. ([64061](https://github.com/WordPress/gutenberg/pull/64061))
+- Fix WampServer links. ([64062](https://github.com/WordPress/gutenberg/pull/64062))
+- FormToggle, ToggleControl: Fix docgen in Storybook. ([64065](https://github.com/WordPress/gutenberg/pull/64065))
+- Provide a better example for the PluginSidebar slotfill. ([64206](https://github.com/WordPress/gutenberg/pull/64206))
+- Update data-core.md to use correct headings. ([64309](https://github.com/WordPress/gutenberg/pull/64309))
+
+
+### Code Quality
+
+- Add margin-bottom lint rules ([64212](https://github.com/WordPress/gutenberg/pull/64212)),([64213](https://github.com/WordPress/gutenberg/pull/64213)) and ([63960](https://github.com/WordPress/gutenberg/pull/63960))
+- Add new useEntityRecordsWithPermissions hook. ([63857](https://github.com/WordPress/gutenberg/pull/63857))
+- Fix deprecated sass usage. ([63990](https://github.com/WordPress/gutenberg/pull/63990))
+- Remove an unnecessary wrapper component. ([63989](https://github.com/WordPress/gutenberg/pull/63989))
+- Theme JSON: Update core theme json resolver class use to Gutenberg version. ([63981](https://github.com/WordPress/gutenberg/pull/63981))
+- Zoom out: Get store action outside the loop. ([63936](https://github.com/WordPress/gutenberg/pull/63936))
+- Remove Speak from device menu selection. ([64115](https://github.com/WordPress/gutenberg/pull/64115))
+
+#### Block Editor
+- BlockDraggable: Remove invalid aria-hidden attribute from button. ([64228](https://github.com/WordPress/gutenberg/pull/64228))
+- FontFamilyControl: Deprecate bottom margin. ([64280](https://github.com/WordPress/gutenberg/pull/64280))
+- Remove unnecessary/incorrect `unlock` call in `setEditorMode` action. ([64073](https://github.com/WordPress/gutenberg/pull/64073))
+
+#### Data Views
+- Formalize text field type definition. ([64168](https://github.com/WordPress/gutenberg/pull/64168))
+- Use items with permissions and avoid hooks to register actions. ([63923](https://github.com/WordPress/gutenberg/pull/63923))
+
+#### DataForm
+- Centralize edit logic in field type definitions. ([64171](https://github.com/WordPress/gutenberg/pull/64171))
+- Move validation logic to the field type definition. ([64164](https://github.com/WordPress/gutenberg/pull/64164))
+
+#### Global Styles
+- Background image: Remove toolspanel placeholder component. ([64242](https://github.com/WordPress/gutenberg/pull/64242))
+- Consolidate theme.json ref and URI resolution. ([64182](https://github.com/WordPress/gutenberg/pull/64182))
+
+#### Plugin
+- Remove compat layers for WP 6.4 and 6.5. ([64096](https://github.com/WordPress/gutenberg/pull/64096))
+- Remove leftover 'WP_Rest_Customizer_Nonces' controller. ([64221](https://github.com/WordPress/gutenberg/pull/64221))
+
+#### Site Editor
+- Use `structuredClone` for deep cloning. ([64203](https://github.com/WordPress/gutenberg/pull/64203))
+
+#### Block Library
+- Add stylelint rule to prevent usage of flex-direction reverse values. ([63081](https://github.com/WordPress/gutenberg/pull/63081))
+- Image Block Lightbox: Fix warning error when resizing. ([63995](https://github.com/WordPress/gutenberg/pull/63995))
+
+#### Icons
+- Fix invalid prop for `homeButton` icon. ([64191](https://github.com/WordPress/gutenberg/pull/64191))
+
+#### Post Editor
+- Remove resolvers hack for post actions. ([64094](https://github.com/WordPress/gutenberg/pull/64094))
+
+#### Components
+- Upgrade Ariakit. ([64066](https://github.com/WordPress/gutenberg/pull/64066))
+
+#### Page Content Focus
+- Fix the 'getBlocksByName' selector call. ([63922](https://github.com/WordPress/gutenberg/pull/63922))
+
+
+### Tools
+
+#### Testing
+- Components: Cleanup flaky unit test `sleep()` hacks. ([64205](https://github.com/WordPress/gutenberg/pull/64205))
+- Fix flaky DataViews list layout end-to-end tests. ([64244](https://github.com/WordPress/gutenberg/pull/64244))
+- Fix typo in 'Verify Core Backport Changelog' job title. ([64058](https://github.com/WordPress/gutenberg/pull/64058))
+- Improve `Button` matrix in visual regression test. ([64120](https://github.com/WordPress/gutenberg/pull/64120))
+- Improve theme.json test failure messages by pretty printing css for a more accurate diff. ([64077](https://github.com/WordPress/gutenberg/pull/64077))
+
+
+## First-time contributors
+
+The following PRs were merged by first-time contributors:
+
+- @Chrico: Block categories - ensure that categories are unique by slug. ([62954](https://github.com/WordPress/gutenberg/pull/62954))
+- @djcowan: Update api-reference.md. ([64325](https://github.com/WordPress/gutenberg/pull/64325))
+- @meteorlxy: CustomSelectControl: Support generic props type. ([63985](https://github.com/WordPress/gutenberg/pull/63985))
+- @Rishit30G: Add WP Studio to list of tools in documentation. ([64327](https://github.com/WordPress/gutenberg/pull/64327))
+- @wzieba: ([64044](https://github.com/WordPress/gutenberg/pull/64044))
+
+
+## Contributors
+
+The following contributors merged PRs in this release:
+
+@aaronrobertshaw @adamsilverstein @afercia @akasunil @Aljullu @amitraj2203 @andrewserong @carolinan @cbravobernal @Chrico @ciampo @creativecoder @DaniGuardiola @DAreRodz @djcowan @ellatrix @jameskoster @jasmussen @jeryj @jorgefilipecosta @jsnajdr @kebbet @kmanijak @Mamaduka @matiasbenedetto @meteorlxy @mikachan @mirka @mtias @ndiego @noisysocks @oandregal @ramonjd @richtabor @Rishit30G @ryanwelcher @SantosGuillamot @scruffian @shail-mehta @simison @stokesman @t-hamano @talldan @tomdevisser @tomjn @tyxla @up1512001 @wzieba @youknowriad
+
+
+
+
= 18.9.0 =
## Changelog
diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md
index f304ec9cd3a487..ec3af0d7bd4cb2 100644
--- a/docs/contributors/code/release.md
+++ b/docs/contributors/code/release.md
@@ -227,7 +227,7 @@ The final step is to write a release post on [make.wordpress.org/core](https://m
> The plugin was published to the WordPress.org plugin directory but the workflow failed.
-This has happened ocassionally, see [this one](https://github.com/WordPress/gutenberg/actions/runs/6955409957/job/18924124118) for example.
+This has happened occasionally, see [this one](https://github.com/WordPress/gutenberg/actions/runs/6955409957/job/18924124118) for example.
It's important to check that:
diff --git a/docs/explanations/architecture/modularity.md b/docs/explanations/architecture/modularity.md
index f94f8ec7b9472e..ff619ccbfdf5b7 100644
--- a/docs/explanations/architecture/modularity.md
+++ b/docs/explanations/architecture/modularity.md
@@ -42,7 +42,7 @@ function MyApp() {
```php
// myplugin.php
-// Example of script registration dependending on the "components" and "element packages.
+// Example of script registration depending on the "components" and "element packages.
wp_register_script( 'myscript', 'pathtomyscript.js', array ('wp-components', "react" ) );
```
diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md
index 39c80262d7bcbe..98c435f6ebe2f7 100644
--- a/docs/getting-started/fundamentals/block-wrapper.md
+++ b/docs/getting-started/fundamentals/block-wrapper.md
@@ -102,7 +102,7 @@ The [example block](https://github.com/WordPress/block-development-examples/tree
## Dynamic render markup
-In dynamic blocks, where the font-end markup is rendered server-side, you can utilize the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to output the necessary classes and attributes just like you would use `useBlockProps.save()` in the `save` function. (See [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11))
+In dynamic blocks, where the front-end markup is rendered server-side, you can utilize the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to output the necessary classes and attributes just like you would use `useBlockProps.save()` in the `save` function. (See [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11))
```php
>
diff --git a/docs/getting-started/fundamentals/static-dynamic-rendering.md b/docs/getting-started/fundamentals/static-dynamic-rendering.md
index 8d199f66cccd2a..dfb6a7123b44b3 100644
--- a/docs/getting-started/fundamentals/static-dynamic-rendering.md
+++ b/docs/getting-started/fundamentals/static-dynamic-rendering.md
@@ -61,7 +61,7 @@ Dynamic blocks, which we'll explore in the following section, can specify an ini
For a practical demonstration of how this works, refer to the [Building your first block](/docs/getting-started/tutorial.md) tutorial. Specifically, the [Adding static rendering](/docs/getting-started/tutorial.md#adding-static-rendering) section illustrates how a block can have both a saved HTML structure and dynamic rendering capabilities.
-WordPress provides mechanisms like the render_block are the $render_callback function to alter the saved HTML of a block before it appears on the front end. These tools offer developers the capability to customize block output dynamically, catering to complex and interactive user experiences.
+WordPress provides mechanisms like the render_block and the render_callback function to alter the saved HTML of a block before it appears on the front end. These tools offer developers the capability to customize block output dynamically, catering to complex and interactive user experiences.
Additional examples of WordPress blocks that use static rendering, meaning their output is fixed at the time of saving and doesn't rely on server-side processing, include:
diff --git a/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md b/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md
index 122ee3eaa0c27e..85ac956ff74ba6 100644
--- a/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md
+++ b/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md
@@ -138,6 +138,17 @@ And a `style.css` file to load on the frontend:
The files will automatically be enqueued when specified in the block.json.
+
+
+If you are using `@wordpress/scripts` you will need to import your stylesheet within your corresponding JavaScript file in order for `@wordpress/scripts` to process the stylesheet.
+
+Example:
+
+- In `edit.js` you would place `import './editor.scss';`
+- In `index.js` you would place `import './style.scss';`
+- In `view.js` you would place `import './view.scss';` (interactive block template)
+
+
**Note:** If you have multiple files to include, you can use standard `wp_enqueue_style` functions like any other plugin or theme. You will want to use the following hooks for the block editor:
- `enqueue_block_editor_assets` - to load only in editor view
diff --git a/docs/manifest.json b/docs/manifest.json
index 1704e6d711510f..e4eba19d99fa29 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -497,6 +497,30 @@
"markdown_source": "../docs/reference-guides/interactivity-api/README.md",
"parent": "reference-guides"
},
+ {
+ "title": "Core Concepts",
+ "slug": "core-concepts",
+ "markdown_source": "../docs/reference-guides/interactivity-api/core-concepts/README.md",
+ "parent": "interactivity-api"
+ },
+ {
+ "title": "The Reactive and Declarative mindset",
+ "slug": "the-reactive-and-declarative-mindset",
+ "markdown_source": "../docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md",
+ "parent": "core-concepts"
+ },
+ {
+ "title": "Understanding global state, local context and derived state",
+ "slug": "undestanding-global-state-local-context-and-derived-state",
+ "markdown_source": "../docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md",
+ "parent": "core-concepts"
+ },
+ {
+ "title": "Server-side rendering: Processing directives on the server",
+ "slug": "server-side-rendering",
+ "markdown_source": "../docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md",
+ "parent": "core-concepts"
+ },
{
"title": "Quick start guide",
"slug": "iapi-quick-start-guide",
@@ -767,6 +791,12 @@
"markdown_source": "../packages/components/src/combobox-control/README.md",
"parent": "components"
},
+ {
+ "title": "Composite",
+ "slug": "composite",
+ "markdown_source": "../packages/components/src/composite/README.md",
+ "parent": "components"
+ },
{
"title": "ConfirmDialog",
"slug": "confirm-dialog",
diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index 5beb712c80a113..b9cae44550181c 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -783,7 +783,7 @@ Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Ju
- **Name:** core/quote
- **Category:** text
-- **Supports:** anchor, background (backgroundImage, backgroundSize), color (background, gradients, heading, link, text), dimensions (minHeight), interactivity (clientNavigation), layout (~~allowEditing~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~
+- **Supports:** align (full, left, right, wide), anchor, background (backgroundImage, backgroundSize), color (background, gradients, heading, link, text), dimensions (minHeight), interactivity (clientNavigation), layout (~~allowEditing~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** citation, textAlign, value
## Read More
diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md
index e7a31c1e3bbc83..637cecadf1402b 100644
--- a/docs/reference-guides/filters/block-filters.md
+++ b/docs/reference-guides/filters/block-filters.md
@@ -139,12 +139,12 @@ The following PHP filters are available to change the output of a block on the f
### `render_block`
-Filters the font-end content of any block. This filter has no impact on the behavior of blocks in the Editor.
+Filters the front-end content of any block. This filter has no impact on the behavior of blocks in the Editor.
The callback function for this filter receives three parameters:
- `$block_content` (`string`): The block content.
-- `block` (`array`): The full block, including name and attributes.
+- `$block` (`array`): The full block, including name and attributes.
- `$instance` (`WP_Block`): The block instance.
In the following example, the class `example-class` is added to all Paragraph blocks on the front end. Here the [HTML API](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2/) is used to easily add the class instead of relying on regex.
@@ -172,12 +172,12 @@ add_filter( 'render_block', 'example_add_custom_class_to_paragraph_block', 10, 2
### `render_block_{namespace/block}`
-Filters the font-end content of the defined block. This is just a simpler form of `render_block` when you only need to modify a specific block type.
+Filters the front-end content of the defined block. This is just a simpler form of `render_block` when you only need to modify a specific block type.
The callback function for this filter receives three parameters:
- `$block_content` (`string`): The block content.
-- `block` (`array`): The full block, including name and attributes.
+- `$block` (`array`): The full block, including name and attributes.
- `$instance` (`WP_Block`): The block instance.
In the following example, the class `example-class` is added to all Paragraph blocks on the front end. Notice that compared to the `render_block` example above, you no longer need to check the block type before modifying the content. Again, the [HTML API](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2/) is used instead of regex.
diff --git a/docs/reference-guides/interactivity-api/README.md b/docs/reference-guides/interactivity-api/README.md
index f5d410a8439f45..3a5bbb84ff159c 100644
--- a/docs/reference-guides/interactivity-api/README.md
+++ b/docs/reference-guides/interactivity-api/README.md
@@ -16,17 +16,17 @@ For more information about the genesis of the Interactivity API, check out the [
Use the following links to locate the topic you're interested in. If you have never worked with the Interactivity API before, consider reading through the following resources in the order listed.
-- **[Requirements](#requirements-of-the-interactivity-api):** Check this section before you start creating your interactive blocks with the Interactivity API.
-- **[Quick Start Guide](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-quick-start-guide/):** Get a custom block using the Interactivity API up and running in less than one minute.
-- **[Tutorial: A first look at the Interactivity API](https://developer.wordpress.org/news/2024/04/11/a-first-look-at-the-interactivity-api/)** This article from the [WordPress Developer Blog](https://developer.wordpress.org/news/) is a great way to get introduced to the Interactivity API.
-- **[API Reference](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/):** To take a deep dive into how the API works internally, the list of Directives, and how the Store works.
-- **[Docs and Examples](#docs-examples):** Additional resources to learn/read more about the Interactivity API.
+- **[Requirements](#requirements-of-the-interactivity-api):** Check this section before you start creating your interactive blocks with the Interactivity API.
+- **[Quick Start Guide](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-quick-start-guide/):** Get a custom block using the Interactivity API up and running in less than one minute.
+- **[Tutorial: A first look at the Interactivity API](https://developer.wordpress.org/news/2024/04/11/a-first-look-at-the-interactivity-api/)** This article from the [WordPress Developer Blog](https://developer.wordpress.org/news/) is a great way to get introduced to the Interactivity API.
+- **[Core Concepts](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/core-concepts/)** Gain a better understanding of concepts and mental models related to Interactivity API development from this section.
+- **[API Reference](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/):** To take a deep dive into how the API works internally, the list of Directives, and how the Store works.
+- **[Docs and Examples](#docs-examples):** Additional resources to learn/read more about the Interactivity API.
To get a deeper understanding of what the Interactivity API is or find answers to questions you may have about this standard, check the following resources:
-- **[About the Interactivity API](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-about/):** To learn more about the API Goals and the reasoning behind the use of a standard to add interactivity to blocks.
-- **[Frequently Asked Questions](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-faq/):** To find responses to some frequently asked questions about the technology behind and alternatives.
-
+- **[About the Interactivity API](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-about/):** To learn more about the API Goals and the reasoning behind the use of a standard to add interactivity to blocks.
+- **[Frequently Asked Questions](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-faq/):** To find responses to some frequently asked questions about the technology behind and alternatives.
## Requirements of the Interactivity API
@@ -34,9 +34,9 @@ Interactivity API is included in Core in WordPress 6.5. For versions below, you'
It’s also important to highlight that the block creation workflow doesn’t change, and all the [prerequisites](https://developer.wordpress.org/block-editor/getting-started/devenv/) remain the same. These include:
-- [Code Editor](https://developer.wordpress.org/block-editor/getting-started/devenv/#code-editor)
-- [Node.js development tools](https://developer.wordpress.org/block-editor/getting-started/devenv/#node-js-development-tools)
-- [Local WordPress environment (site)](https://developer.wordpress.org/block-editor/getting-started/devenv/#local-wordpress-environment)
+- [Code Editor](https://developer.wordpress.org/block-editor/getting-started/devenv/#code-editor)
+- [Node.js development tools](https://developer.wordpress.org/block-editor/getting-started/devenv/#node-js-development-tools)
+- [Local WordPress environment (site)](https://developer.wordpress.org/block-editor/getting-started/devenv/#local-wordpress-environment)
You can start creating interactions once you set up a block development environment and run WordPress 6.5+ (or Gutenberg 17.5+).
@@ -56,7 +56,6 @@ Import the store into your `view.js`. Refer to the [store documentation](https:/
import { store } from '@wordpress/interactivity';
```
-
#### Add `interactivity` support to `block.json`
To indicate that the block [supports](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/) the Interactivity API features, add `"interactivity": true` to the `supports` attribute of the block's `block.json` file.
@@ -84,7 +83,6 @@ The Interactivity API provides the `@wordpress/interactivity` Script Module. Jav
The use of `viewScriptModule` also requires the `--experimental-modules` flag for both the [`build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build) and [`start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start) scripts of `wp-scripts` to ensure a proper build of the Script Modules.
-
```json
// package.json
{
@@ -99,11 +97,9 @@ The use of `viewScriptModule` also requires the `--experimental-modules` flag fo
To "activate" the Interactivity API in a DOM element (and its children), add the [`wp-interactive`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity/packages-interactivity-api-reference/#wp-interactive) directive to the DOM element in the block's `render.php` or `save.js` files.
-
-
```html
-
+
```
@@ -113,17 +109,16 @@ Refer to the [`wp-interactive` documentation](https://developer.wordpress.org/bl
Here you have some more resources to learn/read more about the Interactivity API:
-- [WordPress 6.5 Dev Note](https://make.wordpress.org/core/2024/03/04/interactivity-api-dev-note/)
-- [Merge announcement](https://make.wordpress.org/core/2024/02/19/merge-announcement-interactivity-api/)
-- [Proposal: The Interactivity API – A better developer experience in building interactive blocks](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/)
-- [Interactivity API Discussions](https://github.com/WordPress/gutenberg/discussions/52882), especially the [showcase](https://github.com/WordPress/gutenberg/discussions/55642#discussioncomment-9667164) discussions.
-- [wpmovies.dev](http://wpmovies.dev/) demo and its [wp-movies-demo](https://github.com/WordPress/wp-movies-demo) repo
-- Examples using the Interactivity API at [block-development-examples](https://github.com/WordPress/block-development-examples):
- - [`interactivity-api-block-833d15`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-block-833d15)
- - [`interactivity-api-countdown-3cd73e`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-countdown-3cd73e)
- - [`interactivity-api-quiz-1835fa`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-quiz-1835fa)
+- [WordPress 6.5 Dev Note](https://make.wordpress.org/core/2024/03/04/interactivity-api-dev-note/)
+- [Merge announcement](https://make.wordpress.org/core/2024/02/19/merge-announcement-interactivity-api/)
+- [Proposal: The Interactivity API – A better developer experience in building interactive blocks](https://make.wordpress.org/core/2023/03/30/proposal-the-interactivity-api-a-better-developer-experience-in-building-interactive-blocks/)
+- [Interactivity API Discussions](https://github.com/WordPress/gutenberg/discussions/52882), especially the [showcase](https://github.com/WordPress/gutenberg/discussions/55642#discussioncomment-9667164) discussions.
+- [wpmovies.dev](http://wpmovies.dev/) demo and its [wp-movies-demo](https://github.com/WordPress/wp-movies-demo) repo
+- Examples using the Interactivity API at [block-development-examples](https://github.com/WordPress/block-development-examples):
+ - [`interactivity-api-block-833d15`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-block-833d15)
+ - [`interactivity-api-countdown-3cd73e`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-countdown-3cd73e)
+ - [`interactivity-api-quiz-1835fa`](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/interactivity-api-quiz-1835fa)
-
diff --git a/docs/reference-guides/interactivity-api/api-reference.md b/docs/reference-guides/interactivity-api/api-reference.md
index a4b400b8c0276b..46bd20bece0bda 100644
--- a/docs/reference-guides/interactivity-api/api-reference.md
+++ b/docs/reference-guides/interactivity-api/api-reference.md
@@ -776,7 +776,7 @@ Actions are just regular JavaScript functions. Usually triggered by the `data-wp
```ts
const { state, actions } = store("myPlugin", {
actions: {
- selectItem: (id?: number) => {
+ selectItem: ( id ) => {
const context = getContext();
// `id` is optional here, so this action can be used in a directive.
state.selected = id || context.id;
@@ -1152,7 +1152,7 @@ store('mySliderPlugin', {
## Server functions
-The Interactivity API comes with handy functions on the PHP part. Apart from [setting the store via server](#on-the-server-side), there is also a function to get and set Interactivity related config variables.
+The Interactivity API comes with handy functions that allow you to initialize and reference configuration options on the server. This is necessary to feed the initial data that the Server Directive Processing will use to modify the HTML markup before it's send to the browser. It is also a great way to leverage many of WordPress's APIs, like nonces, AJAX, and translations.
### wp_interactivity_config
@@ -1181,6 +1181,53 @@ This config can be retrieved on the client:
const { showLikeButton } = getConfig();
```
+### wp_interactivity_state
+
+`wp_interactivity_state` allows the initialization of the global state on the server, which will be used to process the directives on the server and then will be merged with any global state defined in the client.
+
+Initializing the global state on the server also allows you to use many critical WordPress APIs, including [AJAX](https://developer.wordpress.org/plugins/javascript/ajax/), or [nonces](https://developer.wordpress.org/plugins/javascript/enqueuing/#nonce).
+
+The `wp_interactivity_state` function receives two arguments, a string with the namespace that will be used as a reference and an associative array containing the values.
+
+Here is an example of passing the WP Admin AJAX endpoint with a nonce.
+
+```php
+// render.php
+
+wp_interactivity_state(
+ 'myPlugin',
+ array(
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'myPlugin_nonce' ),
+ ),
+);
+```
+
+```js
+// view.js
+
+const { state } = store( 'myPlugin', {
+ actions: {
+ *doSomething() {
+ try {
+ const formData = new FormData();
+ formData.append( 'action', 'do_something' );
+ formData.append( '_ajax_nonce', state.nonce );
+
+ const data = yield fetch( state.ajaxUrl, {
+ method: 'POST',
+ body: formData,
+ } ).then( ( response ) => response.json() );
+ console.log( 'Server data!', data );
+ } catch ( e ) {
+ // Something went wrong!
+ }
+ },
+ },
+ }
+);
+```
+
### wp_interactivity_process_directives
`wp_interactivity_process_directives` returns the updated HTML after the directives have been processed.
diff --git a/docs/reference-guides/interactivity-api/core-concepts/README.md b/docs/reference-guides/interactivity-api/core-concepts/README.md
new file mode 100644
index 00000000000000..f4e6891c4ff165
--- /dev/null
+++ b/docs/reference-guides/interactivity-api/core-concepts/README.md
@@ -0,0 +1,9 @@
+# Core Concepts
+
+This section provides some guides on important concepts and mental models related to Interactivity API development. Use the following links to learn more:
+
+1. **[The Reactive and Declarative mindset](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md):** This guide covers core concepts of reactivity and declarativeness, providing a foundation for effective use of the Interactivity API.
+
+2. **[Understanding global state, local context and derived state](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md):** The guide explains how to effectively use global state, local context, and derived state within the Interactivity API emphasizing the importance of choosing the appropriate state management technique based on the scope and requirements of your data.
+
+3. **[Server-side rendering: Processing directives on the server](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md):** The Interactivity API allows WordPress to use server-side rendering to create interactive and state-aware HTML, smoothly connected with client-side features while maintaining performance and SEO benefits.
diff --git a/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md b/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md
new file mode 100644
index 00000000000000..8c62bd82eaba90
--- /dev/null
+++ b/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md
@@ -0,0 +1,491 @@
+# Server-side rendering: Processing directives on the server
+
+WordPress has always been built on the foundation of server-side rendering. Traditionally, when a user requests a WordPress page, the server processes the PHP code, queries the database, and generates the HTML markup that is sent to the browser.
+
+In recent years, modern JavaScript frameworks like Vue, React, or Svelte have revolutionized the way we build web applications. These frameworks provide reactive and declarative programming models that enable developers to create dynamic, interactive user interfaces with ease.
+
+However, when it comes to server-side rendering, these frameworks require a JavaScript-based server, such as NodeJS, to execute their code and generate the initial HTML. This means that PHP-based servers, like WordPress, cannot directly utilize these frameworks without sacrificing their native PHP rendering capabilities. This limitation poses a challenge for WordPress developers who want to leverage the power of reactive and declarative programming while still benefiting from WordPress's traditional server-side rendering strengths. The Interactivity API bridges this gap by bringing [reactive and declarative programming principles](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md) to WordPress without compromising its server-side rendering foundation.
+
+In this guide, we'll explore how the Interactivity API processes directives on the server, enabling WordPress to deliver interactive, state-aware HTML from the initial page load, while setting the stage for seamless client-side interactivity.
+
+## Processing the directives on the server
+
+The Interactivity API's Server Directive Processing capabilities enable WordPress to generate the initial HTML with the correct interactive state, providing a faster initial render. After the initial server-side render, the Interactivity API's client-side JavaScript takes over, enabling dynamic updates and interactions without requiring full page reloads. This approach combines the best of both worlds: the SEO and performance benefits of traditional WordPress server-side rendering, and the dynamic, reactive user interfaces offered by modern JavaScript frameworks.
+
+To understand how the Server Directive Processing works, let's start with an example where a list of fruits is rendered using the `data-wp-each` directive.
+
+The following are the necessary steps to ensure that the directives are correctly processed by the Server Directive Processing of the Interactivity API during the server-side rendering of WordPress.
+
+- **1. Mark the block as interactive**
+
+ First, to enable the server processing of the interactive block's directives, you must add `supports.interactivity` to the `block.json`:
+
+ ```json
+ {
+ "supports": {
+ "interactivity": true
+ }
+ }
+ ```
+
+- **2. Initialize the global state or local context**
+
+ Then, you must initialize either the global state or the local context that will be used during the server-side rendering of the page.
+
+ If you are using global state, you must use the `wp_interactivity_state` function:
+
+ ```php
+ wp_interactivity_state( 'myFruitPlugin', array(
+ 'fruits' => array( 'Apple', 'Banana', 'Cherry' )
+ ));
+ ```
+
+ If you are using local context, the initial values are defined with the `data-wp-context` directive itself, either by:
+
+ - Adding it directly to the HTML.
+
+ ```html
+
+ ```
+
+- **3. Define the interactive elements using directives**
+
+ Next, you need to add the necessary directives to the HTML markup.
+
+ ```html
+
+
+
+
+
+ ```
+
+ In this example:
+
+ - The `data-wp-interactive` directive activates the interactivity for the DOM element and its children.
+ - The `data-wp-each` directive is used to render a list of elements. The directive can be used in `` tags, with the value being a reference path to an array stored in the global state or the local context.
+ - The `data-wp-text` directive sets the inner text of an HTML element. Here, it points to `context.item`, which is where the `data-wp-each` directive stores each item of the array.
+
+ The exact same directives can also be used when using local context instead of global state. The only difference is that `data-wp-each` points to `context.fruits` instead of `state.fruits`:
+
+ ```html
+
+
+
+
+
+ ```
+
+That's it! Once you've set up your interctive block with `supports.interactivity`, initialized your global state or local context, and added the directives to the HTML markup, the Interactivity API will take care of the rest. There's no additional code required from the developer to process these directives on the server side.
+
+Behind the scenes, WordPress uses the `wp_interactivity_process_directives` function to find and process the directives in the HTML markup of your block. This function uses the HTML API to make the necessary changes to the markup, based on the found directives and the initial global state and/or local context.
+
+As a result, the HTML markup sent to the browser is already in its final form, with all directives correctly processed. This means that when the page first loads in the browser, it already contains the correct initial state of all interactive elements, without needing any JavaScript to modify it.
+
+This is how the final HTML markup of the fruit list example would look like (directives omitted):
+
+```html
+
+
Apple
+
Banana
+
Cherry
+
+```
+
+As you can see, the `data-wp-each` directive has generated a `
` element for each fruit in the array, and the `data-wp-text` directive has been processed, populating each `
` with the correct fruit name.
+
+## Manipulating the global state and local context in the client
+
+One of the key strengths of the Interactivity API is how it bridges the gap between server-side rendering and client-side interactivity. To do so, the global state and local context initialized on the server are also serialized and made available to the Interactivity API stores in the client, allowing the application to continue functioning and manipulating the DOM dynamically.
+
+Let's extend this example to include a button that the user can click to add a new fruit to the list:
+
+```html
+
+```
+
+This new button has a `data-wp-on-async--click` directive that references `actions.addMango`, which is defined in our JavaScript store:
+
+```javascript
+const { state } = store( 'myFruitPlugin', {
+ actions: {
+ addMango() {
+ state.fruits.push( 'Mango' );
+ },
+ },
+} );
+```
+
+The same example would also work if you were using local context:
+
+```javascript
+store( 'myFruitPlugin', {
+ actions: {
+ addMango() {
+ const context = getContext();
+ context.fruits.push( 'Mango' );
+ },
+ },
+} );
+```
+
+Now, when the user clicks the "Add Mango" button:
+
+1. The `addMango` action is triggered.
+2. The `'Mango'` item is added to the `state.fruits` (or `context.fruits`) array.
+3. The Interactivity API automatically updates the DOM, adding a new `
` element for the new fruit.
+
+```html
+
+
Apple
+
Banana
+
Cherry
+
Mango
+
+```
+
+Remember: initializing the state on the client is not necessary when it has already been done on the server.
+
+```javascript
+store( 'myFruitPlugin', {
+ state: {
+ fruits: [ 'Apple', 'Banana', 'Cherry' ], // This is not necessary!
+ },
+} );
+```
+
+## Initializing the derived state in the server
+
+The derived state, regardless of whether it derives from the global state, local context, or both, can also be processed on the server by the Server Directive Processing.
+
+_Please, visit the [Understanding global state, local context and derived state](./undestanding-global-state-local-context-and-derived-state.md) guide to learn more about how derived state works in the Interactivity API._
+
+### Derived state that can be defined statically
+
+Let's imagine adding a button that can delete all fruits:
+
+```html
+
+```
+
+```javascript
+const { state } = store( 'myFruitPlugin', {
+ actions: {
+ // ...
+ deleteFruits() {
+ state.fruits = [];
+ },
+ },
+} );
+```
+
+Now, let's display a special message when there is no fruit. To do this, let's use a `data-wp-bind--hidden` directive that references a derived state called `state.hasFruits` to show/hide the message.
+
+```html
+
+
+
+
+
+
+
No fruits, sorry!
+
+```
+
+The derived state `state.hasFruits` is defined on the client using a getter:
+
+```javascript
+const { state } = store( 'myFruitPlugin', {
+ state: {
+ get hasFruits() {
+ return state.fruits.length > 0;
+ },
+ },
+ // ...
+} );
+```
+
+Up to this point, everything is fine in the client, and when we press the "Delete all fruits" button, the "No fruits, sorry!" message will be displayed. The problem is that since `state.hasFruits` is not defined on the server, the `hidden` attribute will not be part of the initial HTML, which means it will also be showing the message until JavaScript loads, causing not only confusion to the visitor, but also a layout shift when JavaScript finally loads and the message is hidden.
+
+To fix this, you must define the initial value of the derived state in the server using `wp_interactivity_state`.
+
+- When the initial value is known and static, it can be defined directly:
+
+ ```php
+ wp_interactivity_state( 'myFruitPlugin', array(
+ 'fruits' => array( 'Apple', 'Banana', 'Cherry' ),
+ 'hasFruits' => true
+ ));
+ ```
+
+- Or it can be defined by doing the necessary computations:
+
+ ```php
+ $fruits = array( 'Apple', 'Banana', 'Cherry' );
+ $hasFruits = count( $fruits ) > 0;
+
+ wp_interactivity_state( 'myFruitPlugin', array(
+ 'fruits' => $fruits,
+ 'hasFruits' => $hasFruits,
+ ));
+ ```
+
+Regardless of the approach, the key point is that the initial value of `state.hasFruits` is now defined on the server. This allows the Server Directive Processing to handle the `data-wp-bind--hidden` directive and modify the HTML markup, adding the `hidden` attribute when needed.
+
+### Derived state that needs to be defined dynamically
+
+In most cases, the initial derived state can be defined statically, as in the previous example. But sometimes, the value depends on dynamic values that also change in the server, and the derived logic needs to be replicated in PHP.
+
+To see an example of this, let's continue by adding a shopping cart emoji (🛒) for each fruit, depending on whether it is on a shopping list or not.
+
+First, let's add an array that represents the shopping list. _Remember that even though these arrays are static for simplicity sake, usually you will work with dynamic information, for example, information coming from the database._
+
+```php
+wp_interactivity_state( 'myFruitPlugin', array(
+ 'fruits' => array( 'Apple', 'Banana', 'Cherry' ),
+ 'shoppingList' => array( 'Apple', 'Cherry' ),
+));
+```
+
+Now, let's add a derived state on the client that checks if each fruit is on the shopping list or not and returns the emoji.
+
+```javascript
+store( 'myFruitPlugin', {
+ state: {
+ get onShoppingList() {
+ const context = getContext();
+ return state.shoppingList.includes( context.item ) ? '🛒' : '';
+ },
+ },
+ // ...
+} );
+```
+
+And let's use that derived state to show the appropriate emoji for each fruit.
+
+```html
+
+
+
+
+
+
+
+
+```
+
+Again, up to this point, everything is fine on the client side and the visitor will see the correct emoji displayed for the fruits they have on their shopping list. However, since `state.onShoppingList` is not defined on the server, the emoji will not be part of the initial HTML, and it will not be shown until JavaScript loads.
+
+Let's fix that by adding the initial derived state using `wp_interactivity_state`. Remember that this time, the value depends on `context.item` that comes from the `data-wp-each` directive, which makes the derived value dynamic, so let's replicate the JavaScript logic in PHP:
+
+```php
+wp_interactivity_state( 'myFruitPlugin', array(
+ // ...
+ 'onShoppingList' => function() {
+ $state = wp_interactivity_state();
+ $context = wp_interactivity_get_context();
+ return in_array( $context['item'], $state['shoppingList'] ) ? '🛒' : '';
+ }
+));
+```
+
+That's it! Now, our server can compute the derived state and know which fruits are on the shopping list and which are not. This allows the Server Directive Processing to populate the initial HTML with the correct values, ensuring that the user sees the correct information immediately, even before the JavaScript runtime loads.
+
+## Serializing other processed values to be consumed on the client
+
+The `wp_interactivity_state` function is also valuable for sending processed values from the server to the client so they can be consumed later on. This feature is useful in many situations, such as managing translations.
+
+Let's add translations to our example to see how this would work.
+
+```php
+ array( __( 'Apple' ), __( 'Banana' ), __( 'Cherry' ) ),
+ 'shoppingList' => array( __( 'Apple' ), __( 'Cherry' ) ),
+ // ...
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+That's it! Since the Interactivity API works in PHP, you can add translations directly to the global state, the local context and the HTML markup.
+
+But wait, what happens with our `addMango` action? Remember, this action is defined only on JavaScript:
+
+```javascript
+const { state } = store( 'myFruitPlugin', {
+ actions: {
+ addMango() {
+ state.fruits.push( 'Mango' ); // Not translated!
+ },
+ },
+} );
+```
+
+To fix this issue, you can use the `wp_interactivity_state` function to serialize the translated mango string and then access that value in your action.
+
+```php
+wp_interactivity_state( 'myFruitPlugin', array(
+ 'fruits' => array( __( 'Apple' ), __( 'Banana' ), __( 'Cherry' ) ),
+ 'mango' => __( 'Mango' ),
+));
+```
+
+```javascript
+const { state } = store( 'myFruitPlugin', {
+ actions: {
+ addMango() {
+ // `state.mango` contains the 'Mango' string already translated.
+ state.fruits.push( state.mango );
+ },
+ },
+} );
+```
+
+Take into account that if your application is more dynamic, you could serialize an array with all the fruit translations and just work with _fruit keywords_ in your actions. For example:
+
+```php
+wp_interactivity_state( 'myFruitPlugin', array(
+ 'fruits' => array( 'apple', 'banana', 'cherry' ),
+ 'translatedFruits' => array(
+ 'apple' => __( 'Apple' ),
+ 'banana' => __( 'Banana' ),
+ 'cherry' => __( 'Cherry' ),
+ 'mango' => __( 'Mango' ),
+ ),
+ 'translatedFruit' => function() {
+ $state = wp_interactivity_state();
+ $context = wp_interactivity_get_context();
+ return $state['translatedFruits'][ $context['item'] ];
+ }
+));
+```
+
+```javascript
+const { state } = store( 'myFruitPlugin', {
+ state: {
+ get translatedFruit() {
+ const context = getContext();
+ return state.translatedFruits[ context.item ];
+ }
+ }
+ actions: {
+ addMango() {
+ state.fruits.push( 'mango' );
+ },
+ },
+} );
+```
+
+```html
+
+
+
+```
+
+Serializing information from the server can also be useful in other scenarios, such as passing Ajax/REST-API URLs and nonces.
+
+```php
+wp_interactivity_state( 'myPlugin', array(
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'myPlugin_nonce' ),
+));
+```
+
+```js
+const { state } = store( 'myPlugin', {
+ actions: {
+ *doSomething() {
+ const formData = new FormData();
+ formData.append( 'action', 'do_something' );
+ formData.append( '_ajax_nonce', state.nonce );
+
+ const data = yield fetch( state.ajaxUrl, {
+ method: 'POST',
+ body: formData,
+ } ).then( ( response ) => response.json() );
+
+ console.log( 'Server data', data );
+ },
+ },
+} );
+```
+
+## Processing directives in classic themes
+
+Server Directive Processing happens automatically in your interactive blocks as soon as you add `supports.interactivity` to your `block.json` file. But what about classic themes?
+
+Classic themes can also use the Interactivity API, and if they want to take advantage of the Server Directive Processing (which they should), they can do so through the `wp_interactivity_process_directives` function. This function receives the HTML markup with unprocessed directives and returns the HTML markup modified according to the initial values of the global state, local context, and derived state.
+
+```php
+// Initializes the global and derived state…
+wp_interactivity_state( '...', /* ... */ );
+
+// The interactive HTML markup that contains the directives.
+$html = '
...
';
+
+// Processes the directives so they are ready to be sent to the client.
+$processed_html = wp_interactivity_process_directives( $html );
+```
+
+That's it! There's nothing else you need to do.
+
+If you want to use `wp_interactivity_process_directives` in a template file, you can use `ob_start` and `ob_get_clean` to capture the HTML output and process it before rendering.
+
+```php
+
+
+
+ ...
+
+
+
+
+
+
this is inactive
+
+
+
+```
+
+As you can see, for each condition, you have to use JavaScript to modify everything in the DOM that has changed, taking into account the previous state.
+
+### The declarative approach
+
+The declarative approach simplifies the process by focusing on _what_ should happen. The UI updates automatically in response to changes in state. Here is a similar example using the Interactivity API's declarative approach:
+
+```html
+
+
+
+
+ this is inactive
+
+
+```
+
+```js
+import { store } from '@wordpress/interactivity';
+
+const { state } = store( 'myInteractivePlugin', {
+ state: {
+ isVisible: false,
+ isActive: false,
+ get visibilityText() {
+ return state.isVisible ? 'hide' : 'show';
+ },
+ get activationText() {
+ return state.isActive ? 'deactivate' : 'activate';
+ },
+ get paragraphText() {
+ return state.isActive ? 'this is active' : 'this is inactive';
+ },
+ },
+ actions: {
+ toggleVisibility() {
+ state.isVisible = ! state.isVisible;
+ if ( ! state.isVisible ) state.isActive = false;
+ },
+ toggleActivation() {
+ state.isActive = ! state.isActive;
+ },
+ },
+} );
+```
+
+In this declarative example, the UI automatically updates based on the current state. All you have to do as developers is to declare the necessary state, any derived state, the actions that modify the state, and which parts of the DOM depend on which parts of the state. The framework takes care of making all the necessary updates to the DOM so that it is always in sync with the current state. The logic remains simple and maintainable regardless of the number of elements controlled by the framework.
+
+### Can you spot the bug?
+
+In the imperative example, a bug has been intentionally introduced for didactical purposes. Can you find it? It's not easy!
+
+
+Show me the answer!
+
+In the case that the Show button is pressed first, then the Activate button, and finally the Hide button, it doesn't add the `inactive` class using `statusParagraph.classList.add('inactive');`. Therefore, the next time the user presses Show, the paragraph will not appear in red.
+
+
+
+These types of bugs are very common in imperative code because you have to manually control all the conditions. On the other hand, they do not exist in declarative code because the framework takes care of updating the DOM and never forgets about anything.
+
+### Benefits of the declarative approach
+
+As demonstrated, the imperative approach requires detailed steps and direct manipulation of the DOM, which can quickly become complex and hard to maintain as the interactivity complexity grows. The more possible states and elements there are, the more conditional logic needs to be added, making the code exponentially more complicated. The declarative approach, on the other hand, simplifies the process by managing the state and letting the framework handle the DOM updates. This leads to more readable, maintainable, and scalable code.
+
+## Reactivity
+
+The Interactivity API is a declarative framework thanks to its leverage of reactivity. In a reactive system, changes to the data automatically trigger updates in the user interface, ensuring that the view always reflects the current state of the application.
+
+### How reactivity works
+
+The Interactivity API uses a fine-grained reactivity system. Here's how it works:
+
+1. **Reactive State**: In the Interactivity API, both the global state and the local context are reactive. This means that when either of these data sources changes, any parts of the UI that depend on them will automatically update.
+
+ - **Global state**: This is global data that can be accessed throughout your interactive blocks.
+ - **Local context**: This is local data that is specific to a particular element and its children.
+ - **Derived State**: In addition to basic state properties, you can define computed properties that automatically update when their dependencies change.
+
+ _Please, visit the [Understanding global state, local context and derived state](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md) guide to learn more about how to work with the different types of reactive state in the Interactivity API._
+
+2. **Actions**: These are functions, usually triggered by event handlers, that mutate the global state or local context.
+
+3. **Reactive Bindings**: HTML elements are bound to reactive state values using special attributes like `data-wp-bind`, `data-wp-text`, or `data-wp-class`.
+
+4. **Automatic Updates**: When the actions mutate the global state or local context, the Interactivity API automatically updates all the parts of the DOM that depend on that state (either directly or through the derived state).
+
+Let's break down these concepts by reviewing the previous example:
+
+```javascript
+const { state } = store( 'myInteractivePlugin', {
+ state: {
+ isVisible: false,
+ isActive: false,
+ get visibilityText() {
+ return state.isVisible ? 'hide' : 'show';
+ },
+ // ... other derived state
+ },
+ actions: {
+ toggleVisibility() {
+ state.isVisible = ! state.isVisible;
+ },
+ // ... other actions
+ },
+} );
+```
+
+In this code:
+
+- `isVisible` and `isActive` are basic state properties.
+- `visibilityText` is a derived state that automatically updates when `isVisible` changes.
+- `toggleVisibility` is an action that modifies the state.
+
+The HTML bindings look like this:
+
+```html
+
+```
+
+Here's how reactivity works in practice:
+
+1. When the button is clicked, it triggers the `toggleVisibility` action.
+2. This action updates `state.isVisible`.
+3. The Interactivity API detects this change and automatically:
+ - Updates the button's text content (because of `data-wp-text="state.visibilityText"`).
+ - Changes the `aria-expanded` attribute (due to `data-wp-bind--aria-expanded="state.isVisible"`).
+ - Updates any other parts of the DOM that depend on `isVisible` or `visibilityText`.
+
+### Mutability vs immutability
+
+Unlike many other reactive frameworks, **the Interactivity API does not require the use of immutability** when updating the global state or the local context. You can directly mutate objects and arrays, and the reactivity system will still work as expected. This can lead to more intuitive and straightforward code in many cases.
+
+For example, you can push a new item to an array like this:
+
+```javascript
+const { state } = store( 'myArrayPlugin', {
+ state: {
+ list: [ 'item 1', 'item 2' ],
+ },
+ actions: {
+ addItem() {
+ // Right:
+ state.list.push( 'new item' );
+
+ // Wrong:
+ state.list = [ ...state.list, 'new item' ]; // Don't do this!
+ },
+ },
+} );
+```
+
+There's no need to create a new array or use the spread operator as you might in other frameworks. The Interactivity API will detect this change and update any parts of the UI that depend on `state.list`.
+
+### Reactive side effects
+
+In addition to automatically updating the UI, the Interactivity API allows you to perform side effects when reactive data changes using directives like `data-wp-watch`. Side effects are useful for tasks like logging, making API calls, or updating other parts of your application that aren't directly tied to the UI.
+
+Here's an example of how you might use `data-wp-watch`:
+
+```html
+
+
Counter:
+
+
+```
+
+```javascript
+store( 'myCounterPlugin', {
+ actions: {
+ increment() {
+ const context = getContext();
+ context.counter += 1;
+ },
+ },
+ callbacks: {
+ logCounter: () => {
+ const context = getContext();
+ console.log( `The counter is now: ${ context.counter }` );
+ },
+ },
+} );
+```
+
+In this example:
+
+1. The `data-wp-context` directive adds a local context with a property `counter` whose value is `0`.
+2. The `data-wp-watch` directive is set to `callbacks.logCounter`.
+3. Every time `context.counter` changes, the `logCounter` callback will be executed.
+4. The `logCounter` callback logs the current counter to the console.
+
+This allows you to create declarative side effects that automatically run in response to data changes. Some other use cases for `data-wp-watch` might include:
+
+- Saving data to `localStorage` when the data changes.
+- Sending analytics events.
+- Changing the focus for accessibility purposes.
+- Updating the page title, meta tags, or `` attributes.
+- Triggering animations.
+
+## Conclusion
+
+As you continue to work with the Interactivity API, remember to think in terms of state, actions, and side effects. Define your data, describe how it should change, and let the Interactivity API handle the rest. This mental shift may take some time, especially if you're used to more imperative programming styles, but by embracing it, you'll unlock the full potential of the Interactivity API to create truly dynamic and interactive WordPress blocks that delight your users.
diff --git a/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md b/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md
new file mode 100644
index 00000000000000..a2b39fc4c77fb2
--- /dev/null
+++ b/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md
@@ -0,0 +1,740 @@
+# Understanding global state, local context and derived state
+
+The Interactivity API offers a powerful framework for creating interactive blocks. To make the most of its capabilities, it's crucial to understand when to use global state, local context, or derived state. This guide will clarify these concepts and provide practical examples to help you decide when to use each one.
+
+Let's start with a brief definition of global state, local context and derived state.
+
+- **Global state:** Global data that can be accessed and modified by any interactive block on the page, allowing different parts of your interactive blocks to stay in sync.
+- **Local context:** Local data defined within a specific element in the HTML structure, accessible only to that element and its children, providing independent state for individual blocks.
+- **Derived state:** Computed values based on global state or local context, dynamically calculated on-demand to ensure consistent data representation without storing redundant data.
+
+Let's now dive into each of these concepts to study them in more detail and provide some examples.
+
+## Global state
+
+**Global state** in the Interactivity API refers to global data that can be accessed and modified by any interactive block on the page. It serves as a shared information hub, allowing different parts of your blocks to communicate and stay in sync. Global state is the ideal mechanism for exchanging information between interactive blocks, regardless of their position in the DOM tree.
+
+You should use global state when:
+
+- You need to share data between multiple interactive blocks that are not directly related in the DOM hierarchy.
+- You want to maintain a single source of truth for certain data across all your interactive blocks.
+- You're dealing with data that affects multiple parts of your UI simultaneously.
+- You want to implement features that are global for the page.
+
+### Working with global state
+
+- **Initializing the global state**
+
+ Typically, the initial global state values should be defined on the server using the `wp_interactivity_state` function:
+
+ ```php
+ // Populates the initial global state values.
+ wp_interactivity_state( 'myPlugin', array(
+ 'isDarkTheme' => true,
+ 'show' => false,
+ 'helloText' => __( 'world' ),
+ ));
+ ```
+
+ These initial global state values will be used during the rendering of the page in PHP to populate the HTML markup that is sent to the browser.
+
+ - HTML markup written in the PHP file by the developer:
+
+ ```html
+
+
+ Hello
+
+
+
+ ```
+
+ - HTML markup after the directives have been processed and it is ready to be sent to the browser:
+
+ ```html
+
+
+ Hello world
+
+
+
+ ```
+
+ _Please, visit [the Server-side Rendering guide](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md) to learn more about how directives are processed on the server._
+
+ In cases where the global state is not used during the rendering of the page in PHP, it can also be defined directly on the client.
+
+ ```js
+ const { state } = store( 'myPlugin', {
+ state: {
+ isLoading: false,
+ },
+ actions: {
+ *loadSomething() {
+ state.isLoading = true;
+ // ...
+ },
+ },
+ } );
+ ```
+
+ _Please note that, although this works, in general it is a good practice to define all the global state on the server._
+
+- **Accessing the global state**
+
+ In the HTML markup, you can access the global state values directly by referencing `state` in the directive attribute values:
+
+ ```html
+
+
+
+ ```
+
+ In JavaScript, the `store` function from the package at `@wordpress/interactivity` works both as a setter and a getter, returning the store of the selected namespace.
+
+ To access the global state in your actions and callbacks, you can use the `state` property of the object returned by the `store` function:
+
+ ```js
+ const myPluginStore = store( 'myPlugin' );
+
+ myPluginStore.state; // This is the state of the 'myPlugin' namespace.
+ ```
+
+ You can also destructure the object returned by `store`:
+
+ ```js
+ const { state } = store( 'myPlugin' );
+ ```
+
+ And you can do the same even if you are defining the store at that moment, which is the most common scenario:
+
+ ```js
+ const { state } = store( 'myPlugin', {
+ state: {
+ // ...
+ },
+ actions: {
+ toggle() {
+ state.show = ! state.show;
+ },
+ },
+ } );
+ ```
+
+ The global state initialized on the server using the `wp_interactivity_state` function is also included in that object because it is automatically serialized from the server to the client:
+
+ ```php
+ wp_interactivity_state( 'myPlugin', array(
+ 'someValue' => 1,
+ ));
+ ```
+
+ ```js
+ const { state } = store( 'myPlugin', {
+ state: {
+ otherValue: 2,
+ },
+ actions: {
+ readGlobalState() {
+ state.someValue; // It exists and its initial value is 1.
+ state.otherValue; // It exists and its initial value is 2.
+ },
+ },
+ } );
+ ```
+
+ Lastly, all calls to the `store` function with the same namespace are merged together:
+
+ ```js
+ store( 'myPlugin', { state: { someValue: 1 } } );
+
+ store( 'myPlugin', { state: { otherValue: 2 } } );
+
+ /* All calls to `store` return a stable reference to the same object, so you
+ * can get a reference to `state` from any of them. */
+ const { state } = store( 'myPlugin' );
+
+ store( 'myPlugin', {
+ actions: {
+ readValues() {
+ state.someValue; // It exists and its initial value is 1.
+ state.otherValue; // It exists and its initial value is 2.
+ },
+ },
+ } );
+ ```
+
+- **Updating the global state**
+
+ To update the global state, all you need to do is mutate the `state` object once you have obtained it from the `store` function:
+
+ ```js
+ const { state } = store( 'myPlugin', {
+ actions: {
+ updateValues() {
+ state.someValue = 3;
+ state.otherValue = 4;
+ },
+ },
+ } );
+ ```
+
+ Changes to the global state will automatically trigger updates in any directives that depend on the modified values.
+
+ _Please, visit [The Reactive and Declarative mindset](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md) guide to learn more about how reactivity works in the Interactivity API._
+
+### Example: Two interactive blocks using global state to communicate
+
+In this example, there are two independent interactive blocks. One displays a counter, and the other a button to increment that counter. These blocks can be positioned anywhere on the page, regardless of the HTML structure. In other words, one does not need to be an inner block of the other.
+
+- **Counter Block**
+
+ ```php
+ 0
+ ));
+ ?>
+
+
+ >
+ Counter:
+
+ ```
+
+- **Increment Block**
+
+ ```php
+
+ >
+
+
+ ```
+
+ ```js
+ const { state } = store( 'myCounterPlugin', {
+ actions: {
+ increment() {
+ state.counter += 1;
+ },
+ },
+ } );
+ ```
+
+In this example:
+
+1. The global state is initialized on the server using `wp_interactivity_state`, setting an initial `counter` of 0.
+2. The Counter Block displays the current counter using `data-wp-text="state.counter"`, which reads from the global state.
+3. The Increment Block contains a button that triggers the `increment` action when clicked, using `data-wp-on-async--click="actions.increment"`.
+4. In JavaScript, the `increment` action directly modifies the global state by incrementing `state.counter`.
+
+Both blocks are independent and can be placed anywhere on the page. They don't need to be nested or directly related in the DOM structure. Multiple instances of these interactive blocks can be added to the page, and they will all share and update the same global counter value.
+
+## Local context
+
+**Local context** in the Interactivity API refers to local data defined within a specific element in the HTML structure. Unlike global state, local context is only accessible to the element where it's defined and its child elements.
+
+The local context is particularly useful when you need independent state for individual interactive blocks, ensuring that each instance of a block can maintain its own unique data without interfering with others.
+
+You should use local context when:
+
+- You need to maintain separate state for multiple instances of the same interactive block.
+- You want to encapsulate data that's only relevant to a specific interactive block and its children.
+- You need to implement features that are isolated to a specific part of your UI.
+
+### Working with local context
+
+- **Initializing the local context**
+
+ The local context is initialized directly within the HTML structure using the `data-wp-context` directive. This directive accepts a JSON string that defines the initial values for that piece of context.
+
+ ```html
+
+
+
+ ```
+
+ You can also initialize the local context on the server using the `wp_interactivity_data_wp_context` PHP helper, which ensures proper escaping and formatting of the stringified values:
+
+ ```php
+ 0 );
+ ?>
+
+
>
+
+
+ ```
+
+- **Accessing the local context**
+
+ In the HTML markup, you can access the local context values directly by referencing `context` in the directive values:
+
+ ```html
+
+
+
+ ```
+
+ In JavaScript, you can access the local context values using the `getContext` function:
+
+ ```js
+ store( 'myPlugin', {
+ actions: {
+ sendAnalyticsEvent() {
+ const { counter } = getContext();
+ myAnalyticsLibrary.sendEvent( 'updated counter', counter );
+ },
+ },
+ callbacks: {
+ logCounter() {
+ const { counter } = getContext();
+ console.log( `Current counter: ${ counter }` );
+ },
+ },
+ } );
+ ```
+
+ The `getContext` function returns the local context of the element that triggered the action/callback execution.
+
+- **Updating the local context**
+
+ To update the local context values in JavaScript, you can modify the object returned by `getContext`:
+
+ ```js
+ store( 'myPlugin', {
+ actions: {
+ increment() {
+ const context = getContext();
+ context.counter += 1;
+ },
+ updateName( event ) {
+ const context = getContext();
+ context.name = event.target.value;
+ },
+ },
+ } );
+ ```
+
+ Changes to the local context will automatically trigger updates in any directives that depend on the modified values.
+
+ _Please, visit [The Reactive and Declarative mindset](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md) guide to learn more about how reactivity works in the Interactivity API._
+
+- **Nesting local contexts**
+
+ Local contexts can be nested, with child contexts inheriting and potentially overriding values from parent contexts:
+
+ ```html
+
+
Theme:
+
Counter:
+
+
+
Theme:
+
Counter:
+
+
+ ```
+
+ In this example, the inner `div` will have a `theme` value of `"dark"`, but will inherit the `counter` value `0` from its parent context.
+
+### Example: One interactive block using local context to have independent state
+
+In this example, there is a single interactive block that shows a counter and can increment it. By using local context, each instance of this block will have its own independent counter, even if multiple blocks are added to the page.
+
+```php
+
+ data-wp-context='{ "counter": 0 }'
+>
+
Counter:
+
+
+```
+
+```js
+store( 'myCounterPlugin', {
+ actions: {
+ increment() {
+ const context = getContext();
+ context.counter += 1;
+ },
+ },
+} );
+```
+
+In this example:
+
+1. A local context with an initial `counter` value of `0` is defined using the `data-wp-context` directive.
+2. The counter is displayed using `data-wp-text="context.counter"`, which reads from the local context.
+3. The increment button uses `data-wp-on-async--click="actions.increment"` to trigger the increment action.
+4. In JavaScript, the `getContext` function is used to access and modify the local context for each block instance.
+
+A user will be able to add multiple instances of this block to a page, and each will maintain its own independent counter. Clicking the "Increment" button on one block will only affect that specific block's counter and not the others.
+
+## Derived state
+
+**Derived state** in the Interactivity API refers to a value that is computed from other parts of the global state or local context. It's calculated on demand rather than stored. It ensures consistency, reduces redundancies, and enhances the declarative nature of your code.
+
+Derived state is a fundamental concept in modern state management, not unique to the Interactivity API. It's also used in other popular state management systems like Redux, where it's called `selectors`, or Preact Signals, where it's known as `computed` values.
+
+Derived state offers several key benefits that make it an essential part of a well-designed application state, including:
+
+1. **Single source of truth:** Derived state encourages you to store only the essential, raw data in your state. Any values that can be calculated from this core data become derived state. This approach reduces the risk of inconsistencies in your interactive blocks.
+
+2. **Automatic updates:** When you use derived state, values are recalculated automatically whenever the underlying data changes. This ensures that all parts of your interactive blocks always have access to the most up-to-date information without manual intervention.
+
+3. **Simplified state management:** By computing values on-demand rather than storing and updating them manually, you reduce the complexity of your state management logic. This leads to cleaner, more maintainable code.
+
+4. **Improved performance:** In many cases, derived state can be optimized to recalculate only when necessary, potentially improving your interactive blocks' performance.
+
+5. **Easier debugging:** With derived state, it's clearer where data originates and how it's transformed. This can make it easier to track down issues in your interactive blocks.
+
+In essence, derived state allows you to express relationships between different pieces of data in your interactive blocks declaratively, instead of imperatively updating related values whenever something changes.
+
+_Please, visit [The Reactive and Declarative mindset](/docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md) guide to learn more about how to leverage declarative coding in the Interactivity API._
+
+You should use derived state:
+
+- When a part of your global state or local context can be computed from other state values.
+- To avoid redundant data that needs to be manually kept in sync.
+- To ensure consistency across your interactive blocks by automatically updating derived values.
+- To simplify your actions by removing the need to update multiple related state properties.
+
+### Working with derived state
+
+- **Initializing the derived state**
+
+ Typically, the derived state should be initialized on the server using the `wp_interactivity_state` function in the exact same way as the global state.
+
+ - When the initial value is known and static, it can be defined directly:
+
+ ```php
+ wp_interactivity_state( 'myCounterPlugin', array(
+ 'counter' => 1, // This is global state.
+ 'double' => 2, // This is derived state.
+ ));
+ ```
+
+ - Or it can be defined by doing the necessary computations:
+
+ ```php
+ $counter = 1;
+ $double = $counter * 2;
+
+ wp_interactivity_state( 'myCounterPlugin', array(
+ 'counter' => $counter, // This is global state.
+ 'double' => $double, // This is derived state.
+ ));
+ ```
+
+ Regardless of the approach, the initial derived state values will be used during the rendering of the page in PHP, and the HTML can be populated with the correct values.
+
+ _Please, visit [the Server-side Rendering guide](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md) to learn more about how directives are processed on the server._
+
+ The same mechanism applies even when the derived state property depends on the local context.
+
+ ```php
+ $counter );
+
+ wp_interactivity_state( 'myCounterPlugin', array(
+ 'double' => $counter * 2, // This is derived state.
+ ));
+ ?>
+
+
+ >
+
+ Counter:
+
+
+ Double:
+
+
+ ```
+
+ In JavaScript, the derived state is defined using getters:
+
+ ```js
+ const { state } = store( 'myCounterPlugin', {
+ state: {
+ get double() {
+ return state.counter * 2;
+ },
+ },
+ } );
+ ```
+
+ Derived state can depend on local context, or local context and global state at the same time.
+
+ ```js
+ const { state } = store( 'myCounterPlugin', {
+ state: {
+ get double() {
+ const { counter } = getContext();
+ // Depends on local context.
+ return counter * 2;
+ },
+ get product() {
+ const { counter } = getContext();
+ // Depends on local context and global state.
+ return counter * state.factor;
+ },
+ },
+ } );
+ ```
+
+ In some cases, when the derived state depends on the local context and the local context can change dynamically in the server, instead of the initial derived state, you can use a function (Closure) that calculates it dynamically.
+
+ ```php
+ array( 1, 2, 3 ),
+ 'factor' => 3,
+ 'product' => function() {
+ $state = wp_interactivity_state();
+ $context = wp_interactivity_get_context();
+ return $context['item'] * $state['factor'];
+ }
+ ));
+ ?>
+
+
+
+
+ ```
+
+ This `data-wp-each` template will render this HTML (directives omitted):
+
+ ```html
+ 3
+ 6
+ 9
+ ```
+
+- **Accessing the derived state**
+
+ In the HTML markup, the syntax for the derived state is the same as the one for the global state, just by referencing `state` in the directive attribute values.
+
+ ```html
+
+ ```
+
+ The same happens in JavaScript. Both global state and derived state can be consumed through the `state` property of the store:
+
+ ```js
+ const { state } = store( 'myCounterPlugin', {
+ // ...
+ actions: {
+ readValues() {
+ state.counter; // Regular state, returns 1.
+ state.double; // Derived state, returns 2.
+ },
+ },
+ } );
+ ```
+
+ This lack of distinction is intentional, allowing developers to consume both derived and global state uniformly, and making them interchangeable in practice.
+
+ You can also access the derived state from another derived state and, thus, create multiple levels of computed values.
+
+ ```js
+ const { state } = store( 'myPlugin', {
+ state: {
+ get double() {
+ return state.counter * 2;
+ },
+ get doublePlusOne() {
+ return state.double + 1;
+ },
+ },
+ } );
+ ```
+
+- **Updating the derived state**
+
+ The derived state cannot be updated directly. To update its values, you need to update the global state or local context on which that derived state depends.
+
+ ```js
+ const { state } = store( 'myCounterPlugin', {
+ // ...
+ actions: {
+ updateValues() {
+ state.counter; // Regular state, returns 1.
+ state.double; // Derived state, returns 2.
+
+ state.counter = 2;
+
+ state.counter; // Regular state, returns 2.
+ state.double; // Derived state, returns 4.
+ },
+ },
+ } );
+ ```
+
+### Example: Not using derived state vs using derived state
+
+Let's consider a scenario where there is a counter and the double value needs to be displayed, and let's compare two approaches: one without derived state and one with derived state.
+
+- **Not using derived state**
+
+ ```js
+ const { state } = store( 'myCounterPlugin', {
+ state: {
+ counter: 1,
+ double: 2,
+ },
+ actions: {
+ increment() {
+ state.counter += 1;
+ state.double = state.counter * 2;
+ },
+ },
+ } );
+ ```
+
+ In this approach, both the `state.counter` and `state.double` values are manually updated in the `increment` action. While this works, it has several drawbacks:
+
+ - It's less declarative.
+ - It can lead to bugs if `state.counter` is updated from multiple places and developers forget to keep `state.double` in sync.
+ - It requires more cognitive load to remember to update related values.
+
+- **Using derived state**
+
+ ```js
+ const { state } = store( 'myCounterPlugin', {
+ state: {
+ counter: 1,
+ get double() {
+ return state.counter * 2;
+ },
+ },
+ actions: {
+ increment() {
+ state.counter += 1;
+ },
+ },
+ } );
+ ```
+
+ In this improved version:
+
+ - `state.double` is defined as a getter, automatically deriving its value from `state.counter`.
+ - The `increment` action only needs to update `state.counter`.
+ - `state.double` is always guaranteed to have the correct value, regardless of how or where `state.counter` is updated.
+
+### Example: Using derived state with local context
+
+Let's now consider a scenario where there is a local context that initializes a counter.
+
+```js
+store( 'myCounterPlugin', {
+ state: {
+ get double() {
+ const { counter } = getContext();
+ return counter * 2;
+ },
+ },
+ actions: {
+ increment() {
+ const context = getContext();
+ context.counter += 1;
+ },
+ },
+} );
+```
+
+```html
+
+
+
+ Double:
+
+
+
+
+
+
+
+ Double:
+
+
+
+
+
+```
+
+In this example, the derived state `state.double` reads from the local context present in each element and returns the correct value for each instance where it is used.
+
+### Example: Using derived state with both local context and global state
+
+Let's now consider a scenario where there are a global tax rate and local product prices and calculate the final price, including tax.
+
+```html
+
+
Product Price: $
+
Tax Rate:
+
Price (inc. tax): $
+
+```
+
+```js
+const { state } = store( 'myProductPlugin', {
+ state: {
+ taxRate: 0.21,
+ get taxRatePercentage() {
+ return `${ state.taxRate * 100 }%`;
+ },
+ get priceWithTax() {
+ const { priceWithoutTax } = getContext();
+ return price * ( 1 + state.taxRate );
+ },
+ },
+ actions: {
+ updateTaxRate( event ) {
+ // Updates the global tax rate.
+ state.taxRate = event.target.value;
+ },
+ updatePrice( event ) {
+ // Updates the local product price.
+ const context = getContext();
+ context.priceWithoutTax = event.target.value;
+ },
+ },
+} );
+```
+
+In this example, `priceWithTax` is derived from both the global `taxRate` and the local `priceWithoutTax`. Every time you update the global state or local context through the `updateTaxRate` or `updatePrice` actions, the Interactivity API recomputes the derived state and updates the necessary parts of the DOM.
+
+By using derived state, you create a more maintainable and less error-prone codebase. It ensures that related state values are always in sync, reduces the complexity of your actions, and makes your code more declarative and easier to reason about.
+
+## Conclusion
+
+Remember, the key to effective state management is to keep your state minimal and avoid redundancy. Use derived state to compute values dynamically, and choose between global state and local context based on the scope and requirements of your data. This will lead to a cleaner, more robust architecture that is easier to debug and maintain.
diff --git a/docs/reference-guides/slotfills/README.md b/docs/reference-guides/slotfills/README.md
index 8b56ed4ce98b41..5ae68cdb5cb071 100644
--- a/docs/reference-guides/slotfills/README.md
+++ b/docs/reference-guides/slotfills/README.md
@@ -70,11 +70,10 @@ export default function PostSummary( { onActionPerformed } ) {
const { isRemovedPostStatusPanel } = useSelect( ( select ) => {
// We use isEditorPanelRemoved to hide the panel if it was programmatically removed. We do
// not use isEditorPanelEnabled since this panel should not be disabled through the UI.
- const { isEditorPanelRemoved, getCurrentPostType } =
+ const { isEditorPanelRemoved } =
select( editorStore );
return {
isRemovedPostStatusPanel: isEditorPanelRemoved( PANEL_NAME ),
- postType: getCurrentPostType(),
};
}, [] );
@@ -85,11 +84,7 @@ export default function PostSummary( { onActionPerformed } ) {
<>
- }
+ onActionPerformed={ onActionPerformed }
/>
diff --git a/docs/toc.json b/docs/toc.json
index fa80ee6c4f4404..719ffa344e3744 100644
--- a/docs/toc.json
+++ b/docs/toc.json
@@ -204,6 +204,19 @@
},
{
"docs/reference-guides/interactivity-api/README.md": [
+ {
+ "docs/reference-guides/interactivity-api/core-concepts/README.md": [
+ {
+ "docs/reference-guides/interactivity-api/core-concepts/the-reactive-and-declarative-mindset.md": []
+ },
+ {
+ "docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md": []
+ },
+ {
+ "docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md": []
+ }
+ ]
+ },
{
"docs/reference-guides/interactivity-api/iapi-quick-start-guide.md": []
},
diff --git a/gutenberg.php b/gutenberg.php
index 6ec0a56f00e74d..2cd33a570f5857 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.5
* Requires PHP: 7.2
- * Version: 19.0.0-rc.1
+ * Version: 19.1.0-rc.1
* Author: Gutenberg Team
* Text Domain: gutenberg
*
diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php
index 811608127f47ed..a1d99133c1fc09 100644
--- a/lib/block-supports/background.php
+++ b/lib/block-supports/background.php
@@ -62,7 +62,7 @@ function gutenberg_render_background_support( $block_content, $block ) {
$background_styles['backgroundSize'] = $background_styles['backgroundSize'] ?? 'cover';
// If the background size is set to `contain` and no position is set, set the position to `center`.
if ( 'contain' === $background_styles['backgroundSize'] && ! $background_styles['backgroundPosition'] ) {
- $background_styles['backgroundPosition'] = 'center';
+ $background_styles['backgroundPosition'] = '50% 50%';
}
}
diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php
index ad8722091c2d48..756ef06c80aa87 100644
--- a/lib/class-wp-theme-json-gutenberg.php
+++ b/lib/class-wp-theme-json-gutenberg.php
@@ -1744,7 +1744,7 @@ protected function get_layout_styles( $block_metadata, $types = array() ) {
$spacing_rule['selector']
);
} else {
- $format = static::ROOT_BLOCK_SELECTOR === $selector ? '.%2$s %3$s' : '%1$s-%2$s %3$s';
+ $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s';
$layout_selector = sprintf(
$format,
$selector,
@@ -2329,7 +2329,7 @@ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
* ```php
* array(
* 'name' => 'property_name',
- * 'value' => 'property_value,
+ * 'value' => 'property_value',
* )
* ```
*
@@ -2338,6 +2338,7 @@ protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
* @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
* @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
* @since 6.6.0 Passing current theme JSON settings to wp_get_typography_font_size_value(). Using style engine to correctly fetch background CSS values.
+ * @since 6.7.0 Allow ref resolution of background properties.
*
* @param array $styles Styles to process.
* @param array $settings Theme settings.
@@ -2381,21 +2382,28 @@ protected static function compute_style_properties( $styles, $settings = array()
$root_variable_duplicates[] = substr( $css_property, $root_style_length );
}
- // Processes background styles.
- if ( 'background' === $value_path[0] && isset( $styles['background'] ) ) {
- /*
- * For user-uploaded images at the block level, assign defaults.
- * Matches defaults applied in the editor and in block supports: background.php.
- */
- if ( static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) {
- $styles['background']['backgroundSize'] = $styles['background']['backgroundSize'] ?? 'cover';
- // If the background size is set to `contain` and no position is set, set the position to `center`.
- if ( 'contain' === $styles['background']['backgroundSize'] && empty( $styles['background']['backgroundPosition'] ) ) {
- $styles['background']['backgroundPosition'] = 'center';
- }
+ /*
+ * Processes background image styles.
+ * If the value is a URL, it will be converted to a CSS `url()` value.
+ * For an uploaded image (images with a database ID), apply size and position
+ * defaults equal to those applied in block supports in lib/background.php.
+ */
+ if ( 'background-image' === $css_property && ! empty( $value ) ) {
+ $background_styles = gutenberg_style_engine_get_styles(
+ array( 'background' => array( 'backgroundImage' => $value ) )
+ );
+
+ $value = $background_styles['declarations'][ $css_property ];
+ }
+ if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) {
+ if ( 'background-size' === $css_property ) {
+ $value = 'cover';
+ }
+ // If the background size is set to `contain` and no position is set, set the position to `center`.
+ if ( 'background-position' === $css_property ) {
+ $background_size = $styles['background']['backgroundSize'] ?? null;
+ $value = 'contain' === $background_size ? '50% 50%' : null;
}
- $background_styles = gutenberg_style_engine_get_styles( array( 'background' => $styles['background'] ) );
- $value = $background_styles['declarations'][ $css_property ] ?? $value;
}
// Skip if empty and not "0" or value represents array of longhand values.
@@ -2463,6 +2471,7 @@ protected static function compute_style_properties( $styles, $settings = array()
* @since 5.8.0
* @since 5.9.0 Added support for values of array type, which are returned as is.
* @since 6.1.0 Added the `$theme_json` parameter.
+ * @since 6.7.0 Added support for background image refs
*
* @param array $styles Styles subtree.
* @param array $path Which property to process.
@@ -2479,15 +2488,17 @@ protected static function get_property_value( $styles, $path, $theme_json = null
}
/*
- * This converts references to a path to the value at that path
- * where the values is an array with a "ref" key, pointing to a path.
+ * Where the current value is an array with a 'ref' key pointing
+ * to a path, this converts that path into the value at that path.
* For example: { "ref": "style.color.background" } => "#fff".
*/
if ( is_array( $value ) && isset( $value['ref'] ) ) {
$value_path = explode( '.', $value['ref'] );
- $ref_value = _wp_array_get( $theme_json, $value_path );
+ $ref_value = _wp_array_get( $theme_json, $value_path, null );
+ // Background Image refs can refer to a string or an array containing a URL string.
+ $ref_value_url = $ref_value['url'] ?? null;
// Only use the ref value if we find anything.
- if ( ! empty( $ref_value ) && is_string( $ref_value ) ) {
+ if ( ! empty( $ref_value ) && ( is_string( $ref_value ) || is_string( $ref_value_url ) ) ) {
$value = $ref_value;
}
@@ -3247,6 +3258,25 @@ public function merge( $incoming ) {
}
}
}
+
+ /*
+ * Style values are merged at the leaf level, however
+ * some values provide exceptions, namely style values that are
+ * objects and represent unique definitions for the style.
+ */
+ $style_nodes = static::get_styles_block_nodes();
+ foreach ( $style_nodes as $style_node ) {
+ $path = $style_node['path'];
+ /*
+ * Background image styles should be replaced, not merged,
+ * as they themselves are specific object definitions for the style.
+ */
+ $background_image_path = array_merge( $path, static::PROPERTIES_METADATA['background-image'] );
+ $content = _wp_array_get( $incoming_data, $background_image_path, null );
+ if ( isset( $content ) ) {
+ _wp_array_set( $this->theme_json, $background_image_path, $content );
+ }
+ }
}
/**
diff --git a/lib/client-assets.php b/lib/client-assets.php
index a159bc53e6a591..62e874d6b06c82 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -487,7 +487,9 @@ function gutenberg_register_packages_styles( $styles ) {
* This hook also exists, and should be backported to Core in future versions.
* However, it is envisaged that Gutenberg will continue to use the Style Engine's `gutenberg_*` functions and `_Gutenberg` classes to aid continuous development.
*
- * See: https://developer.wordpress.org/block-editor/reference-guides/packages/packages-style-engine/
+ * @since 6.1
+ *
+ * @see https://developer.wordpress.org/block-editor/reference-guides/packages/packages-style-engine/
*
* @param array $options {
* Optional. An array of options to pass to gutenberg_style_engine_get_stylesheet_from_context(). Default empty array.
@@ -496,8 +498,6 @@ function gutenberg_register_packages_styles( $styles ) {
* @type bool $prettify Whether to add new lines and indents to output. Default is the test of whether the global constant `SCRIPT_DEBUG` is defined.
* }
*
- * @since 6.1
- *
* @return void
*/
function gutenberg_enqueue_stored_styles( $options = array() ) {
diff --git a/lib/compat/wordpress-6.7/block-templates.php b/lib/compat/wordpress-6.7/block-templates.php
new file mode 100644
index 00000000000000..e270ab226c1d9f
--- /dev/null
+++ b/lib/compat/wordpress-6.7/block-templates.php
@@ -0,0 +1,41 @@
+register( $template_name, $args );
+ }
+}
+
+if ( ! function_exists( 'wp_unregister_block_template' ) ) {
+ /**
+ * Unregister a template.
+ *
+ * @param string $template_name Template name in the form of `plugin_uri//template_name`.
+ * @return true|WP_Error True on success, WP_Error on failure or if the template doesn't exist.
+ */
+ function wp_unregister_block_template( $template_name ) {
+ return WP_Block_Templates_Registry::get_instance()->unregister( $template_name );
+ }
+}
diff --git a/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php b/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php
new file mode 100644
index 00000000000000..ed67dded75ecb1
--- /dev/null
+++ b/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php
@@ -0,0 +1,203 @@
+post_type );
+ } else {
+ $template = get_block_template( $request['id'], $this->post_type );
+ }
+
+ if ( ! $template ) {
+ return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
+ }
+
+ return $this->prepare_item_for_response( $template, $request );
+ }
+
+ /**
+ * Prepare a single template output for response
+ *
+ * @param WP_Block_Template $item Template instance.
+ * @param WP_REST_Request $request Request object.
+ * @return WP_REST_Response Response object.
+ */
+ // @core-merge: Fix wrong author in plugin templates.
+ public function prepare_item_for_response( $item, $request ) {
+ $template = $item;
+
+ $fields = $this->get_fields_for_response( $request );
+
+ if ( 'plugin' !== $item->origin ) {
+ return parent::prepare_item_for_response( $item, $request );
+ }
+ $cloned_item = clone $item;
+ // Set the origin as theme when calling the previous `prepare_item_for_response()` to prevent warnings when generating the author text.
+ $cloned_item->origin = 'theme';
+ $response = parent::prepare_item_for_response( $cloned_item, $request );
+ $data = $response->data;
+
+ if ( rest_is_field_included( 'origin', $fields ) ) {
+ $data['origin'] = 'plugin';
+ }
+
+ if ( rest_is_field_included( 'plugin', $fields ) ) {
+ $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $cloned_item->slug );
+ if ( $registered_template ) {
+ $data['plugin'] = $registered_template->plugin;
+ }
+ }
+
+ if ( rest_is_field_included( 'author_text', $fields ) ) {
+ $data['author_text'] = $this->get_wp_templates_author_text_field( $template );
+ }
+
+ if ( rest_is_field_included( 'original_source', $fields ) ) {
+ $data['original_source'] = $this->get_wp_templates_original_source_field( $template );
+ }
+
+ $response = rest_ensure_response( $data );
+
+ if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
+ $links = $this->prepare_links( $template->id );
+ $response->add_links( $links );
+ if ( ! empty( $links['self']['href'] ) ) {
+ $actions = $this->get_available_actions();
+ $self = $links['self']['href'];
+ foreach ( $actions as $rel ) {
+ $response->add_link( $rel, $self );
+ }
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Returns the source from where the template originally comes from.
+ *
+ * @param WP_Block_Template $template_object Template instance.
+ * @return string Original source of the template one of theme, plugin, site, or user.
+ */
+ // @core-merge: Changed the comments format (from inline to multi-line) in the entire function.
+ private static function get_wp_templates_original_source_field( $template_object ) {
+ if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) {
+ /*
+ * Added by theme.
+ * Template originally provided by a theme, but customized by a user.
+ * Templates originally didn't have the 'origin' field so identify
+ * older customized templates by checking for no origin and a 'theme'
+ * or 'custom' source.
+ */
+ if ( $template_object->has_theme_file &&
+ ( 'theme' === $template_object->origin || (
+ empty( $template_object->origin ) && in_array(
+ $template_object->source,
+ array(
+ 'theme',
+ 'custom',
+ ),
+ true
+ ) )
+ )
+ ) {
+ return 'theme';
+ }
+
+ // Added by plugin.
+ // @core-merge: Removed `$template_object->has_theme_file` check from this if clause.
+ if ( 'plugin' === $template_object->origin ) {
+ return 'plugin';
+ }
+
+ /*
+ * Added by site.
+ * Template was created from scratch, but has no author. Author support
+ * was only added to templates in WordPress 5.9. Fallback to showing the
+ * site logo and title.
+ */
+ if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) {
+ return 'site';
+ }
+ }
+
+ // Added by user.
+ return 'user';
+ }
+
+ /**
+ * Returns a human readable text for the author of the template.
+ *
+ * @param WP_Block_Template $template_object Template instance.
+ * @return string Human readable text for the author.
+ */
+ private static function get_wp_templates_author_text_field( $template_object ) {
+ $original_source = self::get_wp_templates_original_source_field( $template_object );
+ switch ( $original_source ) {
+ case 'theme':
+ $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' );
+ return empty( $theme_name ) ? $template_object->theme : $theme_name;
+ case 'plugin':
+ // @core-merge: Prioritize plugin name instead of theme name for plugin-registered templates.
+ if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'get_plugin_data' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+ if ( isset( $template_object->plugin ) ) {
+ $plugins = wp_get_active_and_valid_plugins();
+
+ foreach ( $plugins as $plugin_file ) {
+ $plugin_basename = plugin_basename( $plugin_file );
+ // Split basename by '/' to get the plugin slug.
+ list( $plugin_slug, ) = explode( '/', $plugin_basename );
+
+ if ( $plugin_slug === $template_object->plugin ) {
+ $plugin_data = get_plugin_data( $plugin_file );
+
+ if ( ! empty( $plugin_data['Name'] ) ) {
+ return $plugin_data['Name'];
+ }
+
+ break;
+ }
+ }
+ }
+
+ /*
+ * Fall back to the theme name if the plugin is not defined. That's needed to keep backwards
+ * compatibility with templates that were registered before the plugin attribute was added.
+ */
+ $plugins = get_plugins();
+ $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) );
+ if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) {
+ return $plugins[ $plugin_basename ]['Name'];
+ }
+ return isset( $template_object->plugin ) ?
+ $template_object->plugin :
+ $template_object->theme;
+ // @core-merge: End of changes to merge in core.
+ case 'site':
+ return get_bloginfo( 'name' );
+ case 'user':
+ $author = get_user_by( 'id', $template_object->author );
+ if ( ! $author ) {
+ return __( 'Unknown author' );
+ }
+ return $author->get( 'display_name' );
+ }
+ }
+}
diff --git a/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php b/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php
new file mode 100644
index 00000000000000..db53f735e13b3d
--- /dev/null
+++ b/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php
@@ -0,0 +1,256 @@
+ $instance` pairs.
+ *
+ * @since 6.7.0
+ * @var WP_Block_Template[] $registered_block_templates Registered templates.
+ */
+ private $registered_templates = array();
+
+ /**
+ * Container for the main instance of the class.
+ *
+ * @since 6.7.0
+ * @var WP_Block_Templates_Registry|null
+ */
+ private static $instance = null;
+
+ /**
+ * Registers a template.
+ *
+ * @since 6.7.0
+ *
+ * @param string $template_name Template name including namespace.
+ * @param array $args Optional. Array of template arguments.
+ * @return WP_Block_Template|WP_Error The registered template on success, or false on failure.
+ */
+ public function register( $template_name, $args = array() ) {
+
+ $template = null;
+
+ $error_message = '';
+ $error_code = '';
+ if ( ! is_string( $template_name ) ) {
+ $error_message = __( 'Template names must be strings.', 'gutenberg' );
+ $error_code = 'template_name_no_string';
+ } elseif ( preg_match( '/[A-Z]+/', $template_name ) ) {
+ $error_message = __( 'Template names must not contain uppercase characters.', 'gutenberg' );
+ $error_code = 'template_name_no_uppercase';
+ } elseif ( ! preg_match( '/^[a-z0-9-]+\/\/[a-z0-9-]+$/', $template_name ) ) {
+ $error_message = __( 'Template names must contain a namespace prefix. Example: my-plugin//my-custom-template', 'gutenberg' );
+ $error_code = 'template_no_prefix';
+ } elseif ( $this->is_registered( $template_name ) ) {
+ /* translators: %s: Template name. */
+ $error_message = sprintf( __( 'Template "%s" is already registered.', 'gutenberg' ), $template_name );
+ $error_code = 'template_already_registered';
+ }
+
+ if ( $error_message ) {
+ _doing_it_wrong(
+ __METHOD__,
+ $error_message,
+ '6.7.0'
+ );
+ return new WP_Error( $error_code, $error_message );
+ }
+
+ if ( ! $template ) {
+ $theme_name = get_stylesheet();
+ list( $plugin, $slug ) = explode( '//', $template_name );
+ $default_template_types = get_default_block_template_types();
+
+ $template = new WP_Block_Template();
+ $template->id = $theme_name . '//' . $slug;
+ $template->theme = $theme_name;
+ $template->plugin = $plugin;
+ $template->author = null;
+ $template->content = isset( $args['content'] ) ? $args['content'] : '';
+ $template->source = 'plugin';
+ $template->slug = $slug;
+ $template->type = 'wp_template';
+ $template->title = isset( $args['title'] ) ? $args['title'] : $template_name;
+ $template->description = isset( $args['description'] ) ? $args['description'] : '';
+ $template->status = 'publish';
+ $template->origin = 'plugin';
+ $template->is_custom = ! isset( $default_template_types[ $template_name ] );
+ $template->post_types = isset( $args['post_types'] ) ? $args['post_types'] : array();
+ }
+
+ $this->registered_templates[ $template_name ] = $template;
+
+ return $template;
+ }
+
+ /**
+ * Retrieves all registered templates.
+ *
+ * @since 6.7.0
+ *
+ * @return WP_Block_Template[]|false Associative array of `$template_name => $template` pairs.
+ */
+ public function get_all_registered() {
+ return $this->registered_templates;
+ }
+
+ /**
+ * Retrieves a registered template by its name.
+ *
+ * @since 6.7.0
+ *
+ * @param string $template_name Template name including namespace.
+ * @return WP_Block_Template|null|false The registered template, or null if it is not registered.
+ */
+ public function get_registered( $template_name ) {
+ if ( ! $this->is_registered( $template_name ) ) {
+ return null;
+ }
+
+ return $this->registered_templates[ $template_name ];
+ }
+
+ /**
+ * Retrieves a registered template by its slug.
+ *
+ * @since 6.7.0
+ *
+ * @param string $template_slug Slug of the template.
+ * @return WP_Block_Template|null The registered template, or null if it is not registered.
+ */
+ public function get_by_slug( $template_slug ) {
+ $all_templates = $this->get_all_registered();
+
+ if ( ! $all_templates ) {
+ return null;
+ }
+
+ foreach ( $all_templates as $template ) {
+ if ( $template->slug === $template_slug ) {
+ return $template;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieves registered templates matching a query.
+ *
+ * @since 6.7.0
+ *
+ * @param array $query {
+ * Arguments to retrieve templates. Optional, empty by default.
+ *
+ * @type string[] $slug__in List of slugs to include.
+ * @type string[] $slug__not_in List of slugs to skip.
+ * @type string $post_type Post type to get the templates for.
+ * }
+ */
+ public function get_by_query( $query = array() ) {
+ $all_templates = $this->get_all_registered();
+
+ if ( ! $all_templates ) {
+ return array();
+ }
+
+ $query = wp_parse_args(
+ $query,
+ array(
+ 'slug__in' => array(),
+ 'slug__not_in' => array(),
+ 'post_type' => '',
+ )
+ );
+ $slugs_to_include = $query['slug__in'];
+ $slugs_to_skip = $query['slug__not_in'];
+ $post_type = $query['post_type'];
+
+ $matching_templates = array();
+ foreach ( $all_templates as $template_name => $template ) {
+ if ( $slugs_to_include && ! in_array( $template->slug, $slugs_to_include, true ) ) {
+ continue;
+ }
+
+ if ( $slugs_to_skip && in_array( $template->slug, $slugs_to_skip, true ) ) {
+ continue;
+ }
+
+ if ( $post_type && ! in_array( $post_type, $template->post_types, true ) ) {
+ continue;
+ }
+
+ $matching_templates[ $template_name ] = $template;
+ }
+
+ return $matching_templates;
+ }
+
+ /**
+ * Checks if a template is registered.
+ *
+ * @since 6.7.0
+ *
+ * @param string $template_name Template name.
+ * @return bool True if the template is registered, false otherwise.
+ */
+ public function is_registered( $template_name ) {
+ return isset( $this->registered_templates[ $template_name ] );
+ }
+
+ /**
+ * Unregisters a template.
+ *
+ * @since 6.7.0
+ *
+ * @param string $template_name Template name including namespace.
+ * @return WP_Block_Template|false The unregistered template on success, or false on failure.
+ */
+ public function unregister( $template_name ) {
+ if ( ! $this->is_registered( $template_name ) ) {
+ _doing_it_wrong(
+ __METHOD__,
+ /* translators: %s: Template name. */
+ sprintf( __( 'Template "%s" is not registered.', 'gutenberg' ), $template_name ),
+ '6.7.0'
+ );
+ /* translators: %s: Template name. */
+ return new WP_Error( 'template_not_registered', __( 'Template "%s" is not registered.', 'gutenberg' ) );
+ }
+
+ $unregistered_template = $this->registered_templates[ $template_name ];
+ unset( $this->registered_templates[ $template_name ] );
+
+ return $unregistered_template;
+ }
+
+ /**
+ * Utility method to retrieve the main instance of the class.
+ *
+ * The instance will be created if it does not exist yet.
+ *
+ * @since 6.7.0
+ *
+ * @return WP_Block_Templates_Registry The main instance.
+ */
+ public static function get_instance() {
+ if ( null === self::$instance ) {
+ self::$instance = new self();
+ }
+
+ return self::$instance;
+ }
+ }
+}
diff --git a/lib/compat/wordpress-6.7/compat.php b/lib/compat/wordpress-6.7/compat.php
new file mode 100644
index 00000000000000..edc8e3fa5fb03f
--- /dev/null
+++ b/lib/compat/wordpress-6.7/compat.php
@@ -0,0 +1,114 @@
+ $value ) {
+ $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $query_result[ $key ]->slug );
+ if ( $registered_template ) {
+ $query_result[ $key ]->plugin = $registered_template->plugin;
+ $query_result[ $key ]->origin =
+ 'theme' !== $query_result[ $key ]->origin && 'theme' !== $query_result[ $key ]->source ?
+ 'plugin' :
+ $query_result[ $key ]->origin;
+ }
+ }
+
+ if ( ! isset( $query['wp_id'] ) ) {
+ $template_files = _gutenberg_get_block_templates_files( $template_type, $query );
+
+ /*
+ * Add templates registered in the template registry. Filtering out the ones which have a theme file.
+ */
+ $registered_templates = WP_Block_Templates_Registry::get_instance()->get_by_query( $query );
+ $matching_registered_templates = array_filter(
+ $registered_templates,
+ function ( $registered_template ) use ( $template_files ) {
+ foreach ( $template_files as $template_file ) {
+ if ( $template_file['slug'] === $registered_template->slug ) {
+ return false;
+ }
+ }
+ return true;
+ }
+ );
+ $query_result = array_merge( $query_result, $matching_registered_templates );
+ }
+
+ return $query_result;
+}
+add_filter( 'get_block_templates', '_gutenberg_add_block_templates_from_registry', 10, 3 );
+
+/**
+ * Hooks into `get_block_template` to add the `plugin` property when necessary.
+ *
+ * @param [WP_Block_Template|null] $block_template The found block template, or null if there isn’t one.
+ * @return [WP_Block_Template|null] The block template that was already found with the plugin property defined if it was registered by a plugin.
+ */
+function _gutenberg_add_block_template_plugin_attribute( $block_template ) {
+ if ( $block_template ) {
+ $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $block_template->slug );
+ if ( $registered_template ) {
+ $block_template->plugin = $registered_template->plugin;
+ $block_template->origin =
+ 'theme' !== $block_template->origin && 'theme' !== $block_template->source ?
+ 'plugin' :
+ $block_template->origin;
+ }
+ }
+
+ return $block_template;
+}
+add_filter( 'get_block_template', '_gutenberg_add_block_template_plugin_attribute', 10, 1 );
+
+/**
+ * Hooks into `get_block_file_template` so templates from the registry are also returned.
+ *
+ * @param WP_Block_Template|null $block_template The found block template, or null if there is none.
+ * @param string $id Template unique identifier (example: 'theme_slug//template_slug').
+ * @return WP_Block_Template|null The block template that was already found or from the registry. In case the template was already found, add the necessary details from the registry.
+ */
+function _gutenberg_add_block_file_templates_from_registry( $block_template, $id ) {
+ if ( $block_template ) {
+ $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $block_template->slug );
+ if ( $registered_template ) {
+ $block_template->plugin = $registered_template->plugin;
+ $block_template->origin =
+ 'theme' !== $block_template->origin && 'theme' !== $block_template->source ?
+ 'plugin' :
+ $block_template->origin;
+ }
+ return $block_template;
+ }
+
+ $parts = explode( '//', $id, 2 );
+
+ if ( count( $parts ) < 2 ) {
+ return $block_template;
+ }
+
+ list( , $slug ) = $parts;
+ return WP_Block_Templates_Registry::get_instance()->get_by_slug( $slug );
+}
+add_filter( 'get_block_file_template', '_gutenberg_add_block_file_templates_from_registry', 10, 2 );
diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php
new file mode 100644
index 00000000000000..081c22c8102914
--- /dev/null
+++ b/lib/compat/wordpress-6.7/rest-api.php
@@ -0,0 +1,100 @@
+name ) {
+ // Fixes post type name. It should be `type/wp_template_part`.
+ $parts_key = array_search( '/wp/v2/types/wp_template-part?context=edit', $paths, true );
+ if ( false !== $parts_key ) {
+ $paths[ $parts_key ] = '/wp/v2/types/wp_template_part?context=edit';
+ }
+ }
+
+ if ( 'core/edit-post' === $context->name ) {
+ $reusable_blocks_key = array_search(
+ add_query_arg(
+ array(
+ 'context' => 'edit',
+ 'per_page' => -1,
+ ),
+ rest_get_route_for_post_type_items( 'wp_block' )
+ ),
+ $paths,
+ true
+ );
+
+ if ( false !== $reusable_blocks_key ) {
+ unset( $paths[ $reusable_blocks_key ] );
+ }
+ }
+
+ return $paths;
+}
+add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_7', 10, 2 );
+
+if ( ! function_exists( 'wp_api_template_registry' ) ) {
+ /**
+ * Hook in to the template and template part post types and modify the rest
+ * endpoint to include modifications to read templates from the
+ * BlockTemplatesRegistry.
+ *
+ * @param array $args Current registered post type args.
+ * @param string $post_type Name of post type.
+ *
+ * @return array
+ */
+ function wp_api_template_registry( $args, $post_type ) {
+ if ( 'wp_template' === $post_type || 'wp_template_part' === $post_type ) {
+ $args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller_6_7';
+ }
+ return $args;
+ }
+}
+add_filter( 'register_post_type_args', 'wp_api_template_registry', 10, 2 );
+
+/**
+ * Adds `plugin` fields to WP_REST_Templates_Controller class.
+ */
+function gutenberg_register_wp_rest_templates_controller_plugin_field() {
+
+ register_rest_field(
+ 'wp_template',
+ 'plugin',
+ array(
+ 'get_callback' => function ( $template_object ) {
+ if ( $template_object ) {
+ $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template_object['slug'] );
+ if ( $registered_template ) {
+ return $registered_template->plugin;
+ }
+ }
+
+ return;
+ },
+ 'update_callback' => null,
+ 'schema' => array(
+ 'type' => 'string',
+ 'description' => __( 'Plugin that registered the template.', 'gutenberg' ),
+ 'readonly' => true,
+ 'context' => array( 'view', 'edit', 'embed' ),
+ ),
+ )
+ );
+}
+add_action( 'rest_api_init', 'gutenberg_register_wp_rest_templates_controller_plugin_field' );
diff --git a/lib/compat/wordpress-6.7/script-modules.php b/lib/compat/wordpress-6.7/script-modules.php
new file mode 100644
index 00000000000000..0a440ec81688d2
--- /dev/null
+++ b/lib/compat/wordpress-6.7/script-modules.php
@@ -0,0 +1,104 @@
+setAccessible( true );
+ $get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' );
+ $get_import_map->setAccessible( true );
+
+ $modules = array();
+ foreach ( array_keys( $get_marked_for_enqueue->invoke( wp_script_modules() ) ) as $id ) {
+ $modules[ $id ] = true;
+ }
+ foreach ( array_keys( $get_import_map->invoke( wp_script_modules() )['imports'] ) as $id ) {
+ $modules[ $id ] = true;
+ }
+
+ foreach ( array_keys( $modules ) as $module_id ) {
+ /**
+ * Filters data associated with a given Script Module.
+ *
+ * Script Modules may require data that is required for initialization or is essential to
+ * have immediately available on page load. These are suitable use cases for this data.
+ *
+ * This is best suited to a minimal set of data and is not intended to replace the REST API.
+ *
+ * If the filter returns no data (an empty array), nothing will be embedded in the page.
+ *
+ * The data for a given Script Module, if provided, will be JSON serialized in a script tag
+ * with an ID like `wp-script-module-data-{$module_id}`.
+ *
+ * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID that
+ * the data is associated with.
+ *
+ * @param array $data The data that should be associated with the array.
+ */
+ $data = apply_filters( "script_module_data_{$module_id}", array() );
+
+ if ( is_array( $data ) && ! empty( $data ) ) {
+ /*
+ * This data will be printed as JSON inside a script tag like this:
+ *
+ *
+ * A script tag must be closed by a sequence beginning with ``. It's impossible to
+ * close a script tag without using `<`. We ensure that `<` is escaped and `/` can
+ * remain unescaped, so `` will be printed as `\u003C/script\u00E3`.
+ *
+ * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E.
+ * - JSON_UNESCAPED_SLASHES: Don't escape /.
+ *
+ * If the page will use UTF-8 encoding, it's safe to print unescaped unicode:
+ *
+ * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`).
+ * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when
+ * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was
+ * before PHP 7.1 without this constant. Available as of PHP 7.1.0.
+ *
+ * The JSON specification requires encoding in UTF-8, so if the generated HTML page
+ * is not encoded in UTF-8 then it's not safe to include those literals. They must
+ * be escaped to avoid encoding issues.
+ *
+ * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements.
+ * @see https://www.php.net/manual/en/json.constants.php for details on these constants.
+ * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing.
+ */
+ $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS;
+ if ( 'UTF-8' !== get_option( 'blog_charset' ) ) {
+ $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
+ }
+
+ wp_print_inline_script_tag(
+ wp_json_encode( $data, $json_encode_flags ),
+ array(
+ 'type' => 'application/json',
+ 'id' => "wp-script-module-data-{$module_id}",
+ )
+ );
+ }
+ }
+}
+
+add_action(
+ 'after_setup_theme',
+ function () {
+ if ( ! has_action( 'wp_footer', array( wp_script_modules(), 'print_script_module_data' ) ) ) {
+ add_action( 'wp_footer', 'gutenberg_print_script_module_data' );
+ }
+
+ if ( ! has_action( 'admin_print_footer_scripts', array( wp_script_modules(), 'print_script_module_data' ) ) ) {
+ add_action( 'admin_print_footer_scripts', 'gutenberg_print_script_module_data' );
+ }
+ },
+ 20
+);
diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php
index 0093c2e974568f..5a14e1418ed6de 100644
--- a/lib/experimental/script-modules.php
+++ b/lib/experimental/script-modules.php
@@ -200,96 +200,3 @@ function gutenberg_dequeue_module( $module_identifier ) {
_deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_dequeue_script_module' );
wp_script_modules()->dequeue( $module_identifier );
}
-
-
-/**
- * Print data associated with Script Modules in Script tags.
- *
- * This embeds data in the page HTML so that it is available on page load.
- *
- * Data can be associated with a given Script Module by using the
- * `script_module_data_{$module_id}` filter.
- *
- * The data for a given Script Module will be JSON serialized in a script tag with an ID
- * like `wp-script-module-data-{$module_id}`.
- */
-function gutenberg_print_script_module_data(): void {
- $get_marked_for_enqueue = new ReflectionMethod( 'WP_Script_Modules', 'get_marked_for_enqueue' );
- $get_marked_for_enqueue->setAccessible( true );
- $get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' );
- $get_import_map->setAccessible( true );
-
- $modules = array();
- foreach ( array_keys( $get_marked_for_enqueue->invoke( wp_script_modules() ) ) as $id ) {
- $modules[ $id ] = true;
- }
- foreach ( array_keys( $get_import_map->invoke( wp_script_modules() )['imports'] ) as $id ) {
- $modules[ $id ] = true;
- }
-
- foreach ( array_keys( $modules ) as $module_id ) {
- /**
- * Filters data associated with a given Script Module.
- *
- * Script Modules may require data that is required for initialization or is essential to
- * have immediately available on page load. These are suitable use cases for this data.
- *
- * This is best suited to a minimal set of data and is not intended to replace the REST API.
- *
- * If the filter returns no data (an empty array), nothing will be embedded in the page.
- *
- * The data for a given Script Module, if provided, will be JSON serialized in a script tag
- * with an ID like `wp-script-module-data-{$module_id}`.
- *
- * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID that
- * the data is associated with.
- *
- * @param array $data The data that should be associated with the array.
- */
- $data = apply_filters( "script_module_data_{$module_id}", array() );
-
- if ( is_array( $data ) && ! empty( $data ) ) {
- /*
- * This data will be printed as JSON inside a script tag like this:
- *
- *
- * A script tag must be closed by a sequence beginning with ``. It's impossible to
- * close a script tag without using `<`. We ensure that `<` is escaped and `/` can
- * remain unescaped, so `` will be printed as `\u003C/script\u00E3`.
- *
- * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E.
- * - JSON_UNESCAPED_SLASHES: Don't escape /.
- *
- * If the page will use UTF-8 encoding, it's safe to print unescaped unicode:
- *
- * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`).
- * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when
- * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was
- * before PHP 7.1 without this constant. Available as of PHP 7.1.0.
- *
- * The JSON specification requires encoding in UTF-8, so if the generated HTML page
- * is not encoded in UTF-8 then it's not safe to include those literals. They must
- * be escaped to avoid encoding issues.
- *
- * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements.
- * @see https://www.php.net/manual/en/json.constants.php for details on these constants.
- * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing.
- */
- $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS;
- if ( 'UTF-8' !== get_option( 'blog_charset' ) ) {
- $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
- }
-
- wp_print_inline_script_tag(
- wp_json_encode( $data, $json_encode_flags ),
- array(
- 'type' => 'application/json',
- 'id' => "wp-script-module-data-{$module_id}",
- )
- );
- }
- }
-}
-
-add_action( 'wp_footer', 'gutenberg_print_script_module_data' );
-add_action( 'admin_print_footer_scripts', 'gutenberg_print_script_module_data' );
diff --git a/lib/experiments-page.php b/lib/experiments-page.php
index b27f6fc2726a2a..fa95923061daff 100644
--- a/lib/experiments-page.php
+++ b/lib/experiments-page.php
@@ -45,7 +45,7 @@ function gutenberg_initialize_experiments_settings() {
add_settings_field(
'gutenberg-sync-collaboration',
- __( 'Live Collaboration and offline persistence ', 'gutenberg' ),
+ __( 'Live Collaboration and offline persistence', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
@@ -93,7 +93,7 @@ function gutenberg_initialize_experiments_settings() {
add_settings_field(
'gutenberg-form-blocks',
- __( 'Form and input blocks ', 'gutenberg' ),
+ __( 'Form and input blocks', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
@@ -105,7 +105,7 @@ function gutenberg_initialize_experiments_settings() {
add_settings_field(
'gutenberg-grid-interactivity',
- __( 'Grid interactivity ', 'gutenberg' ),
+ __( 'Grid interactivity', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
diff --git a/lib/load.php b/lib/load.php
index 5a299f3b696968..b501f0abd1c978 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -40,6 +40,10 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php';
require __DIR__ . '/compat/wordpress-6.6/rest-api.php';
+ // WordPress 6.7 compat.
+ require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php';
+ require __DIR__ . '/compat/wordpress-6.7/rest-api.php';
+
// Plugin specific code.
require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php';
require_once __DIR__ . '/class-wp-rest-edit-site-export-controller-gutenberg.php';
@@ -98,8 +102,12 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.6/post.php';
// WordPress 6.7 compat.
+require __DIR__ . '/compat/wordpress-6.7/block-templates.php';
require __DIR__ . '/compat/wordpress-6.7/blocks.php';
require __DIR__ . '/compat/wordpress-6.7/block-bindings.php';
+require __DIR__ . '/compat/wordpress-6.7/script-modules.php';
+require __DIR__ . '/compat/wordpress-6.7/class-wp-block-templates-registry.php';
+require __DIR__ . '/compat/wordpress-6.7/compat.php';
// Experimental features.
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
diff --git a/package-lock.json b/package-lock.json
index b0eccc961afb78..cead8e172a5575 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "gutenberg",
- "version": "19.0.0-rc.1",
+ "version": "19.1.0-rc.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "gutenberg",
- "version": "19.0.0-rc.1",
+ "version": "19.1.0-rc.1",
"hasInstallScript": true,
"license": "GPL-2.0-or-later",
"dependencies": {
@@ -86,7 +86,7 @@
"@actions/core": "1.9.1",
"@actions/github": "5.0.0",
"@apidevtools/json-schema-ref-parser": "11.6.4",
- "@ariakit/test": "^0.4.0",
+ "@ariakit/test": "^0.4.2",
"@babel/core": "7.24.3",
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-syntax-jsx": "7.24.1",
@@ -99,7 +99,7 @@
"@octokit/rest": "16.26.0",
"@octokit/types": "6.34.0",
"@octokit/webhooks-types": "5.8.0",
- "@playwright/test": "1.45.1",
+ "@playwright/test": "1.46.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@react-native/babel-preset": "0.73.10",
"@react-native/metro-babel-transformer": "0.73.10",
@@ -1527,14 +1527,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@ariakit/core": {
+ "version": "0.4.9",
+ "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.9.tgz",
+ "integrity": "sha512-nV0B/OTK/0iB+P9RC7fudznYZ8eR6rR1F912Zc54e3+wSW5RrRvNOiRxyMrgENidd4R7cCMDw77XJLSBLKgEPQ=="
+ },
"node_modules/@ariakit/test": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.0.tgz",
- "integrity": "sha512-AcrppK61/AbsMDyDS3AxY3WXI6fcL+WedNpJm44Qx603dVYkS/potk0PrD1MfdC6aRt+2bRRj0n9dLN5lVMtbg==",
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.2.tgz",
+ "integrity": "sha512-WXAAiAyTaHV9klntOB81Y+YHyA5iGxy9wXCmjQOfYK5InsuIour+7TVXICUxn2NF0XD6j6OoEJbCVDJ2Y46xEA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@ariakit/core": "0.4.7",
+ "@ariakit/core": "0.4.9",
"@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0"
},
"peerDependencies": {
@@ -1554,12 +1558,6 @@
}
}
},
- "node_modules/@ariakit/test/node_modules/@ariakit/core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz",
- "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==",
- "dev": true
- },
"node_modules/@aw-web-design/x-default-browser": {
"version": "1.4.126",
"resolved": "https://registry.npmjs.org/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz",
@@ -6950,12 +6948,12 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.45.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz",
- "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==",
+ "version": "1.46.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz",
+ "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==",
"dev": true,
"dependencies": {
- "playwright": "1.45.1"
+ "playwright": "1.46.0"
},
"bin": {
"playwright": "cli.js"
@@ -41083,12 +41081,12 @@
"dev": true
},
"node_modules/playwright": {
- "version": "1.45.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz",
- "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==",
+ "version": "1.46.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz",
+ "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==",
"dev": true,
"dependencies": {
- "playwright-core": "1.45.1"
+ "playwright-core": "1.46.0"
},
"bin": {
"playwright": "cli.js"
@@ -41101,9 +41099,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.45.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz",
- "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==",
+ "version": "1.46.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz",
+ "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
@@ -41754,14 +41752,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true
},
- "node_modules/postcss-prefixwrap": {
- "version": "1.41.0",
- "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.41.0.tgz",
- "integrity": "sha512-gmwwAEE+ci3/ZKjUZppTETINlh1QwihY8gCstInuS7ohk353KYItU4d64hvnUvO2GUy29hBGPHz4Ce+qJRi90A==",
- "peerDependencies": {
- "postcss": "*"
- }
- },
"node_modules/postcss-reduce-initial": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz",
@@ -52237,7 +52227,7 @@
"fast-deep-equal": "^3.1.3",
"memize": "^2.1.0",
"postcss": "^8.4.21",
- "postcss-prefixwrap": "^1.41.0",
+ "postcss-prefixwrap": "^1.51.0",
"postcss-urlrebase": "^1.4.0",
"react-autosize-textarea": "^7.1.0",
"react-easy-crop": "^5.0.6",
@@ -52252,6 +52242,15 @@
"react-dom": "^18.0.0"
}
},
+ "packages/block-editor/node_modules/postcss-prefixwrap": {
+ "version": "1.51.0",
+ "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz",
+ "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": "*"
+ }
+ },
"packages/block-editor/node_modules/postcss-urlrebase": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz",
@@ -52621,7 +52620,7 @@
"version": "28.5.0",
"license": "GPL-2.0-or-later",
"dependencies": {
- "@ariakit/react": "^0.4.7",
+ "@ariakit/react": "^0.4.10",
"@babel/runtime": "^7.16.0",
"@emotion/cache": "^11.7.1",
"@emotion/css": "^11.7.1",
@@ -52677,17 +52676,12 @@
"react-dom": "^18.0.0"
}
},
- "packages/components/node_modules/@ariakit/core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz",
- "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q=="
- },
"packages/components/node_modules/@ariakit/react": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz",
- "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==",
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.10.tgz",
+ "integrity": "sha512-c1+6sNLj57aAXrBZMCVGG+OXeFrPAG0TV1jT7oPJcN/KLRs3aCuO3CCJVep/eKepFzzK01kNRGYX3wPT1TXPNw==",
"dependencies": {
- "@ariakit/react-core": "0.4.7"
+ "@ariakit/react-core": "0.4.10"
},
"funding": {
"type": "opencollective",
@@ -52699,11 +52693,11 @@
}
},
"packages/components/node_modules/@ariakit/react-core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz",
- "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==",
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.10.tgz",
+ "integrity": "sha512-r6DZmtHBmSoOj848+RpBwdZy/55YxPhMhfH14JIO2OLn1F6iSFkQwR7AAGpIrlYycWJFSF7KrQu50O+SSfFJdQ==",
"dependencies": {
- "@ariakit/core": "0.4.7",
+ "@ariakit/core": "0.4.9",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
@@ -52980,7 +52974,7 @@
"version": "4.1.0",
"license": "GPL-2.0-or-later",
"dependencies": {
- "@ariakit/react": "^0.4.7",
+ "@ariakit/react": "^0.4.10",
"@babel/runtime": "^7.16.0",
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
@@ -53002,17 +52996,12 @@
"react": "^18.0.0"
}
},
- "packages/dataviews/node_modules/@ariakit/core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz",
- "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q=="
- },
"packages/dataviews/node_modules/@ariakit/react": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz",
- "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==",
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.10.tgz",
+ "integrity": "sha512-c1+6sNLj57aAXrBZMCVGG+OXeFrPAG0TV1jT7oPJcN/KLRs3aCuO3CCJVep/eKepFzzK01kNRGYX3wPT1TXPNw==",
"dependencies": {
- "@ariakit/react-core": "0.4.7"
+ "@ariakit/react-core": "0.4.10"
},
"funding": {
"type": "opencollective",
@@ -53024,11 +53013,11 @@
}
},
"packages/dataviews/node_modules/@ariakit/react-core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz",
- "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==",
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.10.tgz",
+ "integrity": "sha512-r6DZmtHBmSoOj848+RpBwdZy/55YxPhMhfH14JIO2OLn1F6iSFkQwR7AAGpIrlYycWJFSF7KrQu50O+SSfFJdQ==",
"dependencies": {
- "@ariakit/core": "0.4.7",
+ "@ariakit/core": "0.4.9",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
},
@@ -54585,7 +54574,7 @@
"npm": ">=8.19.2"
},
"peerDependencies": {
- "@playwright/test": "^1.45.1",
+ "@playwright/test": "^1.46.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
@@ -56135,22 +56124,19 @@
}
}
},
+ "@ariakit/core": {
+ "version": "0.4.9",
+ "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.9.tgz",
+ "integrity": "sha512-nV0B/OTK/0iB+P9RC7fudznYZ8eR6rR1F912Zc54e3+wSW5RrRvNOiRxyMrgENidd4R7cCMDw77XJLSBLKgEPQ=="
+ },
"@ariakit/test": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.0.tgz",
- "integrity": "sha512-AcrppK61/AbsMDyDS3AxY3WXI6fcL+WedNpJm44Qx603dVYkS/potk0PrD1MfdC6aRt+2bRRj0n9dLN5lVMtbg==",
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@ariakit/test/-/test-0.4.2.tgz",
+ "integrity": "sha512-WXAAiAyTaHV9klntOB81Y+YHyA5iGxy9wXCmjQOfYK5InsuIour+7TVXICUxn2NF0XD6j6OoEJbCVDJ2Y46xEA==",
"dev": true,
"requires": {
- "@ariakit/core": "0.4.7",
+ "@ariakit/core": "0.4.9",
"@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0"
- },
- "dependencies": {
- "@ariakit/core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz",
- "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q==",
- "dev": true
- }
}
},
"@aw-web-design/x-default-browser": {
@@ -60001,12 +59987,12 @@
}
},
"@playwright/test": {
- "version": "1.45.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz",
- "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==",
+ "version": "1.46.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz",
+ "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==",
"dev": true,
"requires": {
- "playwright": "1.45.1"
+ "playwright": "1.46.0"
}
},
"@pmmmwh/react-refresh-webpack-plugin": {
@@ -67255,13 +67241,18 @@
"fast-deep-equal": "^3.1.3",
"memize": "^2.1.0",
"postcss": "^8.4.21",
- "postcss-prefixwrap": "^1.41.0",
+ "postcss-prefixwrap": "^1.51.0",
"postcss-urlrebase": "^1.4.0",
"react-autosize-textarea": "^7.1.0",
"react-easy-crop": "^5.0.6",
"remove-accents": "^0.5.0"
},
"dependencies": {
+ "postcss-prefixwrap": {
+ "version": "1.51.0",
+ "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz",
+ "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw=="
+ },
"postcss-urlrebase": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz",
@@ -67498,7 +67489,7 @@
"@wordpress/components": {
"version": "file:packages/components",
"requires": {
- "@ariakit/react": "^0.4.7",
+ "@ariakit/react": "^0.4.10",
"@babel/runtime": "^7.16.0",
"@emotion/cache": "^11.7.1",
"@emotion/css": "^11.7.1",
@@ -67546,25 +67537,20 @@
"uuid": "^9.0.1"
},
"dependencies": {
- "@ariakit/core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz",
- "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q=="
- },
"@ariakit/react": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz",
- "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==",
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.10.tgz",
+ "integrity": "sha512-c1+6sNLj57aAXrBZMCVGG+OXeFrPAG0TV1jT7oPJcN/KLRs3aCuO3CCJVep/eKepFzzK01kNRGYX3wPT1TXPNw==",
"requires": {
- "@ariakit/react-core": "0.4.7"
+ "@ariakit/react-core": "0.4.10"
}
},
"@ariakit/react-core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz",
- "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==",
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.10.tgz",
+ "integrity": "sha512-r6DZmtHBmSoOj848+RpBwdZy/55YxPhMhfH14JIO2OLn1F6iSFkQwR7AAGpIrlYycWJFSF7KrQu50O+SSfFJdQ==",
"requires": {
- "@ariakit/core": "0.4.7",
+ "@ariakit/core": "0.4.9",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
}
@@ -67754,7 +67740,7 @@
"@wordpress/dataviews": {
"version": "file:packages/dataviews",
"requires": {
- "@ariakit/react": "^0.4.7",
+ "@ariakit/react": "^0.4.10",
"@babel/runtime": "^7.16.0",
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
@@ -67769,25 +67755,20 @@
"remove-accents": "^0.5.0"
},
"dependencies": {
- "@ariakit/core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.7.tgz",
- "integrity": "sha512-GUy/3ZY4kW1KdYHtMZRrRlj5FYbZTAyHlimxHI7Zs0xsC+kAzIf8lopnf67Y9IYLgEMr37KosIV7kwpkJpNs5Q=="
- },
"@ariakit/react": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.7.tgz",
- "integrity": "sha512-uUruuCo1M0Nj2oq1nTwDfUlVTLuoI9xeHP75EkuXX46lg5hzE5vVWbSMO1D6MCy7UwrUx2Ts4IqxdKr97suTwQ==",
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.10.tgz",
+ "integrity": "sha512-c1+6sNLj57aAXrBZMCVGG+OXeFrPAG0TV1jT7oPJcN/KLRs3aCuO3CCJVep/eKepFzzK01kNRGYX3wPT1TXPNw==",
"requires": {
- "@ariakit/react-core": "0.4.7"
+ "@ariakit/react-core": "0.4.10"
}
},
"@ariakit/react-core": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.7.tgz",
- "integrity": "sha512-OogUyQ20cxkRNRuqLI05JbmpR4Lr5HwhUIqnb/sipzt6bkg/3wCXEnUAjfxg3nPjLTMjJ8+ODWmPC9JMJTW/yg==",
+ "version": "0.4.10",
+ "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.10.tgz",
+ "integrity": "sha512-r6DZmtHBmSoOj848+RpBwdZy/55YxPhMhfH14JIO2OLn1F6iSFkQwR7AAGpIrlYycWJFSF7KrQu50O+SSfFJdQ==",
"requires": {
- "@ariakit/core": "0.4.7",
+ "@ariakit/core": "0.4.9",
"@floating-ui/dom": "^1.0.0",
"use-sync-external-store": "^1.2.0"
}
@@ -87337,19 +87318,19 @@
"dev": true
},
"playwright": {
- "version": "1.45.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz",
- "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==",
+ "version": "1.46.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz",
+ "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
- "playwright-core": "1.45.1"
+ "playwright-core": "1.46.0"
}
},
"playwright-core": {
- "version": "1.45.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz",
- "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==",
+ "version": "1.46.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz",
+ "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==",
"dev": true
},
"please-upgrade-node": {
@@ -87820,11 +87801,6 @@
}
}
},
- "postcss-prefixwrap": {
- "version": "1.41.0",
- "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.41.0.tgz",
- "integrity": "sha512-gmwwAEE+ci3/ZKjUZppTETINlh1QwihY8gCstInuS7ohk353KYItU4d64hvnUvO2GUy29hBGPHz4Ce+qJRi90A=="
- },
"postcss-reduce-initial": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz",
diff --git a/package.json b/package.json
index fac57093a852c9..7349e60eb4c210 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gutenberg",
- "version": "19.0.0-rc.1",
+ "version": "19.1.0-rc.1",
"private": true,
"description": "A new WordPress editor experience.",
"author": "The WordPress Contributors",
@@ -98,7 +98,7 @@
"@actions/core": "1.9.1",
"@actions/github": "5.0.0",
"@apidevtools/json-schema-ref-parser": "11.6.4",
- "@ariakit/test": "^0.4.0",
+ "@ariakit/test": "^0.4.2",
"@babel/core": "7.24.3",
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-syntax-jsx": "7.24.1",
@@ -111,7 +111,7 @@
"@octokit/rest": "16.26.0",
"@octokit/types": "6.34.0",
"@octokit/webhooks-types": "5.8.0",
- "@playwright/test": "1.45.1",
+ "@playwright/test": "1.46.0",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.11",
"@react-native/babel-preset": "0.73.10",
"@react-native/metro-babel-transformer": "0.73.10",
diff --git a/packages/base-styles/_colors.scss b/packages/base-styles/_colors.scss
index 2ce58b64e43b8c..e65551e48c783c 100644
--- a/packages/base-styles/_colors.scss
+++ b/packages/base-styles/_colors.scss
@@ -17,7 +17,6 @@ $gray-100: #f0f0f0; // Used for light gray backgrounds.
$white: #fff;
// Opacities & additional colors.
-$dark-theme-focus: $white; // Focus color when the theme is dark.
$dark-gray-placeholder: rgba($gray-900, 0.62);
$medium-gray-placeholder: rgba($gray-900, 0.55);
$light-gray-placeholder: rgba($white, 0.65);
@@ -26,3 +25,6 @@ $light-gray-placeholder: rgba($white, 0.65);
$alert-yellow: #f0b849;
$alert-red: #cc1818;
$alert-green: #4ab866;
+
+// Deprecated, please avoid using these.
+$dark-theme-focus: $white; // Focus color when the theme is dark.
diff --git a/packages/base-styles/_variables.scss b/packages/base-styles/_variables.scss
index 97eb513cf38aeb..0f0c9e6d019ba3 100644
--- a/packages/base-styles/_variables.scss
+++ b/packages/base-styles/_variables.scss
@@ -47,9 +47,25 @@ $radius-x-small: 1px; // Applied to elements like buttons nested within primit
$radius-small: 2px; // Applied to most primitives.
$radius-medium: 4px; // Applied to containers with smaller padding.
$radius-large: 8px; // Applied to containers with larger padding.
-$radius-full: 9999px; // For lozenges.
+$radius-full: 9999px; // For pills.
$radius-round: 50%; // For circles and ovals.
+/**
+ * Elevation scale.
+ */
+
+// For sections and containers that group related content and controls, which may overlap other content. Example: Preview Frame.
+$elevation-x-small: 0 0.7px 1px rgba($black, 0.1), 0 1.2px 1.7px -0.2px rgba($black, 0.1), 0 2.3px 3.3px -0.5px rgba($black, 0.1);
+
+// For components that provide contextual feedback without being intrusive. Generally non-interruptive. Example: Tooltips, Snackbar.
+$elevation-small: 0 0.7px 1px 0 rgba(0, 0, 0, 0.12), 0 2.2px 3.7px -0.2px rgba(0, 0, 0, 0.12), 0 5.3px 7.3px -0.5px rgba(0, 0, 0, 0.12);
+
+// For components that offer additional actions. Example: Menus, Command Palette
+$elevation-medium: 0 0.7px 1px 0 rgba(0, 0, 0, 0.14), 0 4.2px 5.7px -0.2px rgba(0, 0, 0, 0.14), 0 7.3px 9.3px -0.5px rgba(0, 0, 0, 0.14);
+
+// For components that confirm decisions or handle necessary interruptions. Example: Modals.
+$elevation-large: 0 0.7px 1px rgba($black, 0.15), 0 2.7px 3.8px -0.2px rgba($black, 0.15), 0 5.5px 7.8px -0.3px rgba($black, 0.15), 0.1px 11.5px 16.4px -0.5px rgba($black, 0.15);
+
/**
* Dimensions.
*/
@@ -74,14 +90,6 @@ $modal-width-large: 840px;
$spinner-size: 16px;
$canvas-padding: $grid-unit-20;
-
-/**
- * Shadows.
- */
-
-$shadow-popover: 0 0.7px 1px rgba($black, 0.1), 0 1.2px 1.7px -0.2px rgba($black, 0.1), 0 2.3px 3.3px -0.5px rgba($black, 0.1);
-$shadow-modal: 0 0.7px 1px rgba($black, 0.15), 0 2.7px 3.8px -0.2px rgba($black, 0.15), 0 5.5px 7.8px -0.3px rgba($black, 0.15), 0.1px 11.5px 16.4px -0.5px rgba($black, 0.15);
-
/**
* Editor widths.
*/
@@ -107,6 +115,8 @@ $radio-input-size-sm: 24px; // Width & height for small viewports.
// Deprecated, please avoid using these.
$block-padding: 14px; // Used to define space between block footprint and surrouding borders.
$radius-block-ui: $radius-small;
+$shadow-popover: $elevation-x-small;
+$shadow-modal: $elevation-large;
/**
diff --git a/packages/block-directory/src/components/downloadable-block-icon/style.scss b/packages/block-directory/src/components/downloadable-block-icon/style.scss
index 57a792b3f7f6e3..e11c1f05e864f8 100644
--- a/packages/block-directory/src/components/downloadable-block-icon/style.scss
+++ b/packages/block-directory/src/components/downloadable-block-icon/style.scss
@@ -3,5 +3,5 @@
width: $button-size * 1.5;
height: $button-size * 1.5;
vertical-align: middle;
- border: 1px solid $gray-300;
+ border: $border-width solid $gray-300;
}
diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md
index 776b217ba54f6e..c798015804b3e5 100644
--- a/packages/block-editor/README.md
+++ b/packages/block-editor/README.md
@@ -920,20 +920,15 @@ _Usage_
import { useBlockProps } from '@wordpress/block-editor';
export default function Edit() {
-
- const blockProps = useBlockProps(
- className: 'my-custom-class',
- style: {
- color: '#222222',
- backgroundColor: '#eeeeee'
- }
- )
-
- return (
-
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
+
+
+`Composite` provides a single tab stop on the page and allows navigation through the focusable descendants with arrow keys. This abstract component is based on the [WAI-ARIA Composite Role](https://w3c.github.io/aria/#composite).
+
+## Usage
+
+```jsx
+import { Composite, useCompositeStore } from '@wordpress/components';
+
+const store = useCompositeStore();
+
+
+ Label
+ Item 1
+ Item 2
+
+
+```
+
+## Hooks
+
+### `useCompositeStore`
+
+Creates a composite store.
+
+#### Props
+
+##### `activeId`: `string | null`
+
+The current active item `id`. The active item is the element within the composite widget that has either DOM or virtual focus (in case the `virtualFocus` prop is enabled).
+
+- `null` represents the base composite element (the one with a [composite role](https://w3c.github.io/aria/#composite)). Users will be able to navigate out of it using arrow keys.
+- If `activeId` 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.
+
+- Required: no
+
+##### `defaultActiveId`: `string | null`
+
+The composite item id that should be active by default when the composite widget is rendered. If `null`, the composite 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.
+
+- Required: no
+
+##### `setActiveId`: `((activeId: string | null | undefined) => void)`
+
+A callback that gets called when the `activeId` state changes.
+
+- Required: no
+
+##### `focusLoop`: `boolean | 'horizontal' | 'vertical' | 'both'`
+
+Determines how the focus behaves when the user reaches the end of the composite widget.
+
+On one-dimensional composite widgets:
+
+- `true` loops from the last item to the first item and vice-versa.
+- `horizontal` loops only if `orientation` is `horizontal` or not set.
+- `vertical` loops only if `orientation` is `vertical` or not set.
+- If `activeId` is initially set to `null`, the composite element will be focused in between the last and first items.
+
+On two-dimensional composite widgets (ie. when using `CompositeRow`):
+
+- `true` loops from the last row/column item to the first item in the same row/column and vice-versa. If it's the last item in the last row, it moves to the first item in the first row and vice-versa.
+- `horizontal` loops only from the last row item to the first item in the same row.
+- `vertical` loops only from the last column item to the first item in the column row.
+- If `activeId` is initially set to `null`, vertical loop will have no effect as moving down from the last row or up from the first row will focus on the composite element.
+- If `focusWrap` matches the value of `focusLoop`, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.
+
+- Required: no
+- Default: `false`
+
+##### `focusShift`: `boolean`
+
+**Works only on two-dimensional composite widgets**.
+
+If enabled, moving up or down when there's no next item or when the next item is disabled will shift to the item right before it.
+
+- Required: no
+- Default: `false`
+
+##### `focusWrap`: `boolean`
+
+**Works only on two-dimensional composite widgets**.
+
+If enabled, moving to the next item from the last one in a row or column
+will focus on the first item in the next row or column and vice-versa.
+
+- `true` wraps between rows and columns.
+- `horizontal` wraps only between rows.
+- `vertical` wraps only between columns.
+- If `focusLoop` matches the value of `focusWrap`, it'll wrap between the
+ last item in the last row or column and the first item in the first row or
+ column and vice-versa.
+
+- Required: no
+- Default: `false`
+
+##### `virtualFocus`: `boolean`
+
+If enabled, the composite element will act as an [`aria-activedescendant`](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant)
+container instead of [roving tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex). DOM focus will remain on the composite element while its items receive
+virtual focus.
+
+In both scenarios, the item in focus will carry the `data-active-item` attribute.
+
+- Required: no
+- Default: `false`
+
+##### `orientation`: `'horizontal' | 'vertical' | 'both'`
+
+Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the `orientation` value 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.
+
+It doesn't have any effect on two-dimensional composites.
+
+- Required: no
+- Default: `both`
+
+##### `rtl`: `boolean`
+
+Determines how the `store`'s `next` and `previous` functions will behave. If `rtl` is set to `true`, they will be inverted.
+
+This only affects the composite widget behavior. You still need to set `dir="rtl"` on HTML/CSS.
+
+- Required: no
+- Default: `false`
+
+## Components
+
+### `Composite`
+
+Renders a composite widget.
+
+#### Props
+
+##### `store`: `CompositeStore`
+
+Object returned by the `useCompositeStore` hook.
+
+- Required: yes
+
+##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>`
+
+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.
+
+- Required: no
+
+##### `focusable`: `boolean`
+
+Makes the component a focusable element. When this element gains keyboard focus, it gets a `data-focus-visible` attribute and triggers the `onFocusVisible` prop.
+
+The component supports the `disabled` prop even for those elements not supporting the native `disabled` attribute. Disabled elements may be still accessible via keyboard by using the the `accessibleWhenDisabled` prop.
+
+Non-native focusable elements will lose their focusability entirely. However, native focusable elements will retain their inherent focusability.
+
+- Required: no
+
+##### `disabled`: `boolean`
+
+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.
+
+**Note**: For this prop to work, the `focusable` prop must be set to
+`true`, if it's not set by default.
+
+- Required: no
+- Default: `false`
+
+##### `accessibleWhenDisabled`: `boolean`
+
+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).
+
+- Required: no
+
+##### `onFocusVisible`: `(event: SyntheticEvent) => void`
+
+Custom event handler invoked when the element gains focus through keyboard interaction or a key press occurs while the element is in focus. This is the programmatic equivalent of the `data-focus-visible` attribute.
+
+**Note**: For this prop to work, the `focusable` prop must be set to `true` if it's not set by default.
+
+- Required: no
+
+##### `children`: `React.ReactNode`
+
+The contents of the component.
+
+- Required: no
+
+### `Composite.Group`
+
+Renders a group element for composite items.
+
+##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>`
+
+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.
+
+- Required: no
+
+##### `children`: `React.ReactNode`
+
+The contents of the component.
+
+- Required: no
+
+### `Composite.GroupLabel`
+
+Renders a label in a composite group. This component must be wrapped with `Composite.Group` so the `aria-labelledby` prop is properly set on the composite group element.
+
+##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>`
+
+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.
+
+- Required: no
+
+##### `children`: `React.ReactNode`
+
+The contents of the component.
+
+- Required: no
+
+### `Composite.Item`
+
+Renders a composite item.
+
+##### `accessibleWhenDisabled`: `boolean`
+
+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).
+
+- Required: no
+
+##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>`
+
+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.
+
+- Required: no
+
+##### `children`: `React.ReactNode`
+
+The contents of the component.
+
+- Required: no
+
+### `Composite.Row`
+
+Renders a composite row. Wrapping `Composite.Item` elements within `Composite.Row` will create a two-dimensional composite widget, such as a grid.
+
+##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>`
+
+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.
+
+- Required: no
+
+##### `children`: `React.ReactNode`
+
+The contents of the component.
+
+- Required: no
+
+### `Composite.Hover`
+
+Renders an element in a composite widget that receives focus on mouse move and loses focus to the composite base element on mouse leave. This should be combined with the `Composite.Item` component.
+
+##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>`
+
+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.
+
+- Required: no
+
+##### `children`: `React.ReactNode`
+
+The contents of the component.
+
+- Required: no
+
+### `Composite.Typeahead`
+
+Renders a component that adds typeahead functionality to composite components. Hitting printable character keys will move focus to the next composite item that begins with the input characters.
+
+##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>`
+
+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.
+
+- Required: no
+
+##### `children`: `React.ReactNode`
+
+The contents of the component.
+
+- Required: no
+
+### `Composite.Context`
+
+The React context used by the composite components. It can be used by to access the composite store, and to forward the context when composite sub-components are rendered across portals (ie. `SlotFill` components) that would not otherwise forward the context to the `Fill` children.
diff --git a/packages/components/src/composite/context.ts b/packages/components/src/composite/context.ts
new file mode 100644
index 00000000000000..69a052c5bfba19
--- /dev/null
+++ b/packages/components/src/composite/context.ts
@@ -0,0 +1,14 @@
+/**
+ * WordPress dependencies
+ */
+import { createContext, useContext } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import type { CompositeContextProps } from './types';
+
+export const CompositeContext =
+ createContext< CompositeContextProps >( undefined );
+
+export const useCompositeContext = () => useContext( CompositeContext );
diff --git a/packages/components/src/composite/current/index.ts b/packages/components/src/composite/current/index.ts
deleted file mode 100644
index 96379f00296516..00000000000000
--- a/packages/components/src/composite/current/index.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Composite is a component that may contain navigable items represented by
- * CompositeItem. It's inspired by the WAI-ARIA Composite Role and implements
- * all the keyboard navigation mechanisms to ensure that there's only one
- * tab stop for the whole Composite element. This means that it can behave as
- * a roving tabindex or aria-activedescendant container.
- *
- * @see https://ariakit.org/components/composite
- */
-
-export {
- Composite,
- CompositeGroup,
- CompositeGroupLabel,
- CompositeItem,
- CompositeRow,
- useCompositeStore,
-} from '@ariakit/react';
-
-export type { CompositeStore, CompositeStoreProps } from '@ariakit/react';
diff --git a/packages/components/src/composite/current/stories/index.story.tsx b/packages/components/src/composite/current/stories/index.story.tsx
deleted file mode 100644
index 335ebc3244c918..00000000000000
--- a/packages/components/src/composite/current/stories/index.story.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * External dependencies
- */
-import type { Meta, StoryFn } from '@storybook/react';
-
-/**
- * WordPress dependencies
- */
-import { isRTL } from '@wordpress/i18n';
-
-/**
- * Internal dependencies
- */
-import {
- Composite,
- CompositeGroup,
- CompositeRow,
- CompositeItem,
- useCompositeStore,
-} from '..';
-import { UseCompositeStorePlaceholder, transform } from './utils';
-
-const meta: Meta< typeof UseCompositeStorePlaceholder > = {
- title: 'Components/Composite (V2)',
- component: UseCompositeStorePlaceholder,
- subcomponents: {
- // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
- Composite,
- // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
- CompositeGroup,
- // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
- CompositeRow,
- // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
- CompositeItem,
- },
- tags: [ 'status-private' ],
- parameters: {
- docs: {
- canvas: { sourceState: 'shown' },
- source: { transform },
- extractArgTypes: ( component: React.FunctionComponent ) => {
- const name = component.displayName;
- const path = name
- ?.replace(
- /([a-z])([A-Z])/g,
- ( _, a, b ) => `${ a }-${ b.toLowerCase() }`
- )
- .toLowerCase();
- const url = `https://ariakit.org/reference/${ path }`;
- return {
- props: {
- name: 'Props',
- description: `See Ariakit docs for ${ name }`,
- table: { type: { summary: undefined } },
- },
- };
- },
- },
- },
-};
-export default meta;
-
-export const Default: StoryFn< typeof Composite > = ( { ...initialState } ) => {
- const rtl = isRTL();
- const store = useCompositeStore( { rtl, ...initialState } );
-
- return (
-
-
- Item A1
- Item A2
- Item A3
-
-
- Item B1
- Item B2
- Item B3
-
-
- Item C1
- Item C2
- Item C3
-
-
- );
-};
diff --git a/packages/components/src/composite/index.ts b/packages/components/src/composite/index.ts
deleted file mode 100644
index aa06a6adf36ef2..00000000000000
--- a/packages/components/src/composite/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// Originally this pointed at a Reakit implementation of
-// `Composite`, but we are removing Reakit entirely from the
-// codebase. We will continue to support the Reakit API
-// through the 'legacy' version, which uses Ariakit under
-// the hood.
-
-export * from './legacy';
diff --git a/packages/components/src/composite/index.tsx b/packages/components/src/composite/index.tsx
new file mode 100644
index 00000000000000..0bfcec2bf76600
--- /dev/null
+++ b/packages/components/src/composite/index.tsx
@@ -0,0 +1,341 @@
+/**
+ * Composite is a component that may contain navigable items represented by
+ * Composite.Item. It's inspired by the WAI-ARIA Composite Role and implements
+ * all the keyboard navigation mechanisms to ensure that there's only one
+ * tab stop for the whole Composite element. This means that it can behave as
+ * a roving tabindex or aria-activedescendant container.
+ *
+ * @see https://ariakit.org/components/composite
+ */
+
+/**
+ * External dependencies
+ */
+import * as Ariakit from '@ariakit/react';
+
+/**
+ * WordPress dependencies
+ */
+import { useMemo, forwardRef } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import type { WordPressComponentProps } from '../context';
+import { CompositeContext, useCompositeContext } from './context';
+import type {
+ CompositeStoreProps,
+ CompositeProps,
+ CompositeGroupProps,
+ CompositeGroupLabelProps,
+ CompositeItemProps,
+ CompositeRowProps,
+ CompositeHoverProps,
+ CompositeTypeaheadProps,
+} from './types';
+
+/**
+ * Creates a composite store.
+ *
+ * @example
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ *
+ * Item
+ * Item
+ * Item
+ *
+ * ```
+ */
+export function useCompositeStore( {
+ focusLoop = false,
+ focusWrap = false,
+ focusShift = false,
+ virtualFocus = false,
+ orientation = 'both',
+ rtl = false,
+ ...props
+}: CompositeStoreProps = {} ) {
+ return Ariakit.useCompositeStore( {
+ focusLoop,
+ focusWrap,
+ focusShift,
+ virtualFocus,
+ orientation,
+ rtl,
+ ...props,
+ } );
+}
+
+const Group = forwardRef<
+ HTMLDivElement,
+ WordPressComponentProps< CompositeGroupProps, 'div', false >
+>( function CompositeGroup( props, ref ) {
+ const context = useCompositeContext();
+ return (
+
+ );
+} );
+Group.displayName = 'Composite.Group';
+
+const GroupLabel = forwardRef<
+ HTMLDivElement,
+ WordPressComponentProps< CompositeGroupLabelProps, 'div', false >
+>( function CompositeGroupLabel( props, ref ) {
+ const context = useCompositeContext();
+ return (
+
+ );
+} );
+GroupLabel.displayName = 'Composite.GroupLabel';
+
+const Item = forwardRef<
+ HTMLButtonElement,
+ WordPressComponentProps< CompositeItemProps, 'button', false >
+>( function CompositeItem( props, ref ) {
+ const context = useCompositeContext();
+ return (
+
+ );
+} );
+Item.displayName = 'Composite.Item';
+
+const Row = forwardRef<
+ HTMLDivElement,
+ WordPressComponentProps< CompositeRowProps, 'div', false >
+>( function CompositeRow( props, ref ) {
+ const context = useCompositeContext();
+ return (
+
+ );
+} );
+Row.displayName = 'Composite.Row';
+
+const Hover = forwardRef<
+ HTMLDivElement,
+ WordPressComponentProps< CompositeHoverProps, 'div', false >
+>( function CompositeHover( props, ref ) {
+ const context = useCompositeContext();
+ return (
+
+ );
+} );
+Hover.displayName = 'Composite.Hover';
+
+const Typeahead = forwardRef<
+ HTMLDivElement,
+ WordPressComponentProps< CompositeTypeaheadProps, 'div', false >
+>( function CompositeTypeahead( props, ref ) {
+ const context = useCompositeContext();
+ return (
+
+ );
+} );
+Typeahead.displayName = 'Composite.Typeahead';
+
+/**
+ * Renders a widget based on the WAI-ARIA [`composite`](https://w3c.github.io/aria/#composite)
+ * role, which provides a single tab stop on the page and arrow key navigation
+ * through the focusable descendants.
+ *
+ * @example
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ *
+ * Item 1
+ * Item 2
+ *
+ * ```
+ */
+export const Composite = Object.assign(
+ forwardRef<
+ HTMLDivElement,
+ WordPressComponentProps< CompositeProps, 'div', false >
+ >( function Composite(
+ { children, store, disabled = false, ...props },
+ ref
+ ) {
+ const contextValue = useMemo(
+ () => ( {
+ store,
+ } ),
+ [ store ]
+ );
+
+ return (
+
+
+ { children }
+
+
+ );
+ } ),
+ {
+ displayName: 'Composite',
+ /**
+ * Renders a group element for composite items.
+ *
+ * @example
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ *
+ *
+ * Label
+ * Item 1
+ * Item 2
+ *
+ *
+ * ```
+ */
+ Group,
+ /**
+ * Renders a label in a composite group. This component must be wrapped with
+ * `Composite.Group` so the `aria-labelledby` prop is properly set on the
+ * composite group element.
+ *
+ * @example
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ *
+ *
+ * Label
+ * Item 1
+ * Item 2
+ *
+ *
+ * ```
+ */
+ GroupLabel,
+ /**
+ * Renders a composite item.
+ *
+ * @example
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ *
+ * Item 1
+ * Item 2
+ * Item 3
+ *
+ * ```
+ */
+ Item,
+ /**
+ * Renders a composite row. Wrapping `Composite.Item` elements within
+ * `Composite.Row` will create a two-dimensional composite widget, such as a
+ * grid.
+ *
+ * @example
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ *
+ *
+ * Item 1.1
+ * Item 1.2
+ * Item 1.3
+ *
+ *
+ * Item 2.1
+ * Item 2.2
+ * Item 2.3
+ *
+ *
+ * ```
+ */
+ Row,
+ /**
+ * Renders an element in a composite widget that receives focus on mouse move
+ * and loses focus to the composite base element on mouse leave. This should
+ * be combined with the `Composite.Item` component.
+ *
+ * @example
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ *
+ * }>
+ * Item 1
+ *
+ * }>
+ * Item 2
+ *
+ *
+ * ```
+ */
+ Hover,
+ /**
+ * Renders a component that adds typeahead functionality to composite
+ * components. Hitting printable character keys will move focus to the next
+ * composite item that begins with the input characters.
+ *
+ * @example
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ * }>
+ * Item 1
+ * Item 2
+ *
+ * ```
+ */
+ Typeahead,
+ /**
+ * The React context used by the composite components. It can be used by
+ * to access the composite store, and to forward the context when composite
+ * sub-components are rendered across portals (ie. `SlotFill` components)
+ * that would not otherwise forward the context to the `Fill` children.
+ *
+ * @example
+ * ```jsx
+ * import { Composite } from '@wordpress/components';
+ * import { useContext } from '@wordpress/element';
+ *
+ * const compositeContext = useContext( Composite.Context );
+ * ```
+ */
+ Context: CompositeContext,
+ }
+);
diff --git a/packages/components/src/composite/legacy/index.tsx b/packages/components/src/composite/legacy/index.tsx
index 5c5c674b5086b8..dffdc1a2066d47 100644
--- a/packages/components/src/composite/legacy/index.tsx
+++ b/packages/components/src/composite/legacy/index.tsx
@@ -5,6 +5,11 @@
* tab stop for the whole Composite element. This means that it can behave as
* a roving tabindex or aria-activedescendant container.
*
+ * This file aims at providing components that are as close as possible to the
+ * original `reakit`-based implementation (which was removed from the codebase),
+ * although it is recommended that consumers of the package switch to the stable,
+ * un-prefixed, `ariakit`-based version of `Composite`.
+ *
* @see https://ariakit.org/components/composite
*/
@@ -16,7 +21,7 @@ import { forwardRef } from '@wordpress/element';
/**
* Internal dependencies
*/
-import * as Current from '../current';
+import { Composite as Current, useCompositeStore } from '..';
import { useInstanceId } from '@wordpress/compose';
type Orientation = 'horizontal' | 'vertical';
@@ -73,7 +78,7 @@ export interface LegacyStateOptions {
type Component = React.FunctionComponent< any >;
-type CompositeStore = ReturnType< typeof Current.useCompositeStore >;
+type CompositeStore = ReturnType< typeof useCompositeStore >;
type CompositeStoreState = { store: CompositeStore };
export type CompositeState = CompositeStoreState &
Required< Pick< LegacyStateOptions, 'baseId' > >;
@@ -93,9 +98,9 @@ type CompositeComponent< C extends Component > = (
) => React.ReactElement;
type CompositeComponentProps = CompositeState &
(
- | ComponentProps< typeof Current.CompositeGroup >
- | ComponentProps< typeof Current.CompositeItem >
- | ComponentProps< typeof Current.CompositeRow >
+ | ComponentProps< typeof Current.Group >
+ | ComponentProps< typeof Current.Item >
+ | ComponentProps< typeof Current.Row >
);
function mapLegacyStatePropsToComponentProps(
@@ -145,19 +150,15 @@ function proxyComposite< C extends Component >(
// provided role, and returning the appropriate component.
const unproxiedCompositeGroup = forwardRef<
any,
- React.ComponentPropsWithoutRef<
- typeof Current.CompositeGroup | typeof Current.CompositeRow
- >
+ React.ComponentPropsWithoutRef< typeof Current.Group | typeof Current.Row >
>( ( { role, ...props }, ref ) => {
- const Component =
- role === 'row' ? Current.CompositeRow : Current.CompositeGroup;
+ const Component = role === 'row' ? Current.Row : Current.Group;
return ;
} );
-unproxiedCompositeGroup.displayName = 'CompositeGroup';
-export const Composite = proxyComposite( Current.Composite, { baseId: 'id' } );
+export const Composite = proxyComposite( Current, { baseId: 'id' } );
export const CompositeGroup = proxyComposite( unproxiedCompositeGroup );
-export const CompositeItem = proxyComposite( Current.CompositeItem, {
+export const CompositeItem = proxyComposite( Current.Item, {
focusable: 'accessibleWhenDisabled',
} );
@@ -178,7 +179,7 @@ export function useCompositeState(
return {
baseId: useInstanceId( Composite, 'composite', baseId ),
- store: Current.useCompositeStore( {
+ store: useCompositeStore( {
defaultActiveId,
rtl,
orientation,
diff --git a/packages/components/src/composite/legacy/stories/utils.tsx b/packages/components/src/composite/legacy/stories/utils.tsx
index 06edd348634695..2fb51c845f9fbe 100644
--- a/packages/components/src/composite/legacy/stories/utils.tsx
+++ b/packages/components/src/composite/legacy/stories/utils.tsx
@@ -8,6 +8,25 @@ import type { StoryContext } from '@storybook/react';
*/
import type { LegacyStateOptions } from '..';
+/**
+ * Renders a composite widget.
+ *
+ * This unstable component is deprecated. Use `Composite` instead.
+ *
+ * ```jsx
+ * import {
+ * __unstableUseCompositeState as useCompositeState,
+ * __unstableComposite as Composite,
+ * __unstableCompositeItem as CompositeItem,
+ * } from '@wordpress/components';
+ *
+ * const state = useCompositeState();
+ *
+ * Item 1
+ * Item 2
+ * ;
+ * ```
+ */
export function UseCompositeStatePlaceholder( props: LegacyStateOptions ) {
return (
diff --git a/packages/components/src/composite/stories/index.story.tsx b/packages/components/src/composite/stories/index.story.tsx
new file mode 100644
index 00000000000000..034e1d6721f7bd
--- /dev/null
+++ b/packages/components/src/composite/stories/index.story.tsx
@@ -0,0 +1,466 @@
+/**
+ * External dependencies
+ */
+import type { Meta, StoryFn } from '@storybook/react';
+
+/**
+ * WordPress dependencies
+ */
+import { isRTL } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import { Composite, useCompositeStore } from '..';
+import { UseCompositeStorePlaceholder, transform } from './utils';
+
+const meta: Meta< typeof UseCompositeStorePlaceholder > = {
+ title: 'Components/Composite (V2)',
+ component: UseCompositeStorePlaceholder,
+ subcomponents: {
+ // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
+ Composite,
+ // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
+ 'Composite.Group': Composite.Group,
+ // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
+ 'Composite.GroupLabel': Composite.GroupLabel,
+ // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
+ 'Composite.Row': Composite.Row,
+ // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
+ 'Composite.Item': Composite.Item,
+ // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
+ 'Composite.Hover': Composite.Hover,
+ // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
+ 'Composite.Typeahead': Composite.Typeahead,
+ },
+ argTypes: {
+ activeId: { control: 'text' },
+ defaultActiveId: { control: 'text' },
+ setActiveId: { control: { type: null } },
+ focusLoop: {
+ control: 'select',
+ options: [ true, false, 'horizontal', 'vertical', 'both' ],
+ },
+ focusShift: { control: 'boolean' },
+ focusWrap: { control: 'boolean' },
+ virtualFocus: { control: 'boolean' },
+ rtl: { control: 'boolean' },
+ orientation: {
+ control: 'select',
+ options: [ 'horizontal', 'vertical', 'both' ],
+ },
+ },
+ tags: [ 'status-private' ],
+ parameters: {
+ controls: { expanded: true },
+ docs: {
+ canvas: { sourceState: 'shown' },
+ source: { transform },
+ extractArgTypes: ( component: React.FunctionComponent ) => {
+ const commonArgTypes = {
+ render: {
+ name: 'render',
+ description:
+ '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.',
+ table: {
+ type: {
+ summary:
+ 'RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>',
+ },
+ },
+ },
+ children: {
+ name: 'children',
+ description: 'The contents of the component.',
+ table: { type: { summary: 'React.ReactNode' } },
+ },
+ };
+ const accessibleWhenDisabled = {
+ name: 'accessibleWhenDisabled',
+ description: `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).`,
+ table: {
+ type: {
+ summary: 'boolean',
+ },
+ },
+ };
+
+ const argTypes = {
+ useCompositeStore: {
+ activeId: {
+ name: 'activeId',
+ description: `The current active item \`id\`. The active item is the element within the composite widget that has either DOM or virtual focus (in case the \`virtualFocus\` prop is enabled).
+- \`null\` represents the base composite element (the one with a [composite role](https://w3c.github.io/aria/#composite)). Users will be able to navigate out of it using arrow keys.
+- If \`activeId\` 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.`,
+ table: { type: { summary: 'string | null' } },
+ },
+ defaultActiveId: {
+ name: 'defaultActiveId',
+ description:
+ 'The composite item id that should be active by default when the composite widget is rendered. If `null`, the composite 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.',
+ table: { type: { summary: 'string | null' } },
+ },
+ setActiveId: {
+ name: 'setActiveId',
+ description:
+ 'A callback that gets called when the `activeId` state changes.',
+ table: {
+ type: {
+ summary:
+ '((activeId: string | null | undefined) => void)',
+ },
+ },
+ },
+ focusLoop: {
+ name: 'focusLoop',
+ description: `On one-dimensional composite widgets:
+
+- \`true\` loops from the last item to the first item and vice-versa.
+- \`horizontal\` loops only if \`orientation\` is \`horizontal\` or not set.
+- \`vertical\` loops only if \`orientation\` is \`vertical\` or not set.
+- If \`activeId\` is initially set to \`null\`, the composite element will be focused in between the last and first items.
+
+On two-dimensional composite widgets (ie. when using \`CompositeRow\`):
+
+- \`true\` loops from the last row/column item to the first item in the same row/column and vice-versa. If it's the last item in the last row, it moves to the first item in the first row and vice-versa.
+- \`horizontal\` loops only from the last row item to the first item in the same row.
+- \`vertical\` loops only from the last column item to the first item in the column row.
+- If \`activeId\` is initially set to \`null\`, vertical loop will have no effect as moving down from the last row or up from the first row will focus on the composite element.
+- If \`focusWrap\` matches the value of \`focusLoop\`, it'll wrap between the last item in the last row or column and the first item in the first row or column and vice-versa.`,
+ table: {
+ defaultValue: {
+ summary: 'false',
+ },
+ type: {
+ summary:
+ "boolean | 'horizontal' | 'vertical' | 'both'",
+ },
+ },
+ },
+ focusShift: {
+ name: 'focusShift',
+ description: `**Works only on two-dimensional composite widgets**.
+
+If enabled, moving up or down when there's no next item or when the next item is disabled will shift to the item right before it.`,
+ table: {
+ defaultValue: {
+ summary: 'false',
+ },
+ type: {
+ summary: 'boolean',
+ },
+ },
+ },
+ focusWrap: {
+ name: 'focusWrap',
+ description: `**Works only on two-dimensional composite widgets**.
+
+If enabled, moving to the next item from the last one in a row or column
+will focus on the first item in the next row or column and vice-versa.
+
+- \`true\` wraps between rows and columns.
+- \`horizontal\` wraps only between rows.
+- \`vertical\` wraps only between columns.
+- If \`focusLoop\` matches the value of \`focusWrap\`, it'll wrap between the
+ last item in the last row or column and the first item in the first row or
+ column and vice-versa.`,
+ table: {
+ defaultValue: {
+ summary: 'false',
+ },
+ type: {
+ summary: 'boolean',
+ },
+ },
+ },
+ virtualFocus: {
+ name: 'virtualFocus',
+ description: `If enabled, the composite element will act as an [\`aria-activedescendant\`](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant)
+container instead of [roving tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex). DOM focus will remain on the composite element while its items receive
+virtual focus.
+
+In both scenarios, the item in focus will carry the \`data-active-item\` attribute.`,
+ table: {
+ defaultValue: {
+ summary: 'false',
+ },
+ type: {
+ summary: 'boolean',
+ },
+ },
+ },
+ orientation: {
+ name: 'orientation',
+ description: `Defines the orientation of the composite widget. If the composite has a single row or column (one-dimensional), the \`orientation\` value 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.
+
+It doesn't have any effect on two-dimensional composites.`,
+ table: {
+ defaultValue: {
+ summary: "'both'",
+ },
+ type: {
+ summary:
+ "'horizontal' | 'vertical' | 'both'",
+ },
+ },
+ },
+ rtl: {
+ name: 'rtl',
+ description: `Determines how the \`store\`'s \`next\` and \`previous\` functions will behave. If \`rtl\` is set to \`true\`, they will be inverted.
+
+This only affects the composite widget behavior. You still need to set \`dir="rtl"\` on HTML/CSS.`,
+ table: {
+ defaultValue: {
+ summary: 'false',
+ },
+ type: {
+ summary: 'boolean',
+ },
+ },
+ },
+ },
+ Composite: {
+ ...commonArgTypes,
+ store: {
+ name: 'store',
+ description:
+ 'Object returned by the `useCompositeStore` hook.',
+ table: {
+ type: {
+ summary:
+ 'CompositeStore',
+ },
+ },
+ type: { required: true },
+ },
+ focusable: {
+ name: 'focusable',
+ description: `Makes the component a focusable element. When this element gains keyboard focus, it gets a \`data-focus-visible\` attribute and triggers the \`onFocusVisible\` prop.
+
+The component supports the \`disabled\` prop even for those elements not supporting the native \`disabled\` attribute. Disabled elements may be still accessible via keyboard by using the the \`accessibleWhenDisabled\` prop.
+
+Non-native focusable elements will lose their focusability entirely. However, native focusable elements will retain their inherent focusability.`,
+ table: {
+ type: {
+ summary: 'boolean',
+ },
+ },
+ },
+ disabled: {
+ name: 'disabled',
+ description: `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.
+
+**Note**: For this prop to work, the \`focusable\` prop must be set to
+\`true\`, if it's not set by default.`,
+ table: {
+ defaultValue: {
+ summary: 'false',
+ },
+ type: {
+ summary: 'boolean',
+ },
+ },
+ },
+ accessibleWhenDisabled,
+ onFocusVisible: {
+ name: 'onFocusVisible',
+ description: `Custom event handler invoked when the element gains focus through keyboard interaction or a key press occurs while the element is in focus. This is the programmatic equivalent of the \`data-focus-visible\` attribute.
+
+**Note**: For this prop to work, the \`focusable\` prop must be set to \`true\` if it's not set by default.`,
+ table: {
+ type: {
+ summary:
+ '(event: SyntheticEvent) => void',
+ },
+ },
+ },
+ },
+ 'Composite.Group': commonArgTypes,
+ 'Composite.GroupLabel': commonArgTypes,
+ 'Composite.Row': commonArgTypes,
+ 'Composite.Item': {
+ ...commonArgTypes,
+ accessibleWhenDisabled,
+ },
+ 'Composite.Hover': commonArgTypes,
+ 'Composite.Typeahead': commonArgTypes,
+ };
+
+ const name = component.displayName ?? '';
+
+ return name in argTypes
+ ? argTypes[ name as keyof typeof argTypes ]
+ : {};
+ },
+ },
+ },
+ decorators: [
+ ( Story ) => {
+ return (
+ <>
+ { /* Visually style the active composite item */ }
+
+
+
+ The active composite item is highlighted with a
+ different background color;
+
+
+ A composite item can be the active item even
+ when it doesn't have keyboard focus.
+
+
+
+ >
+ );
+ },
+ ],
+};
+export default meta;
+
+export const Default: StoryFn< typeof UseCompositeStorePlaceholder > = (
+ storeProps
+) => {
+ const rtl = isRTL();
+ const store = useCompositeStore( { rtl, ...storeProps } );
+
+ return (
+
+ Item one
+ Item two
+ Item three
+
+ );
+};
+
+export const Groups: StoryFn< typeof UseCompositeStorePlaceholder > = (
+ storeProps
+) => {
+ const rtl = isRTL();
+ const store = useCompositeStore( { rtl, ...storeProps } );
+
+ return (
+
+
+ Group one
+ Item 1.1
+ Item 1.2
+
+
+ Group two
+ Item 2.1
+ Item 2.1
+
+
+ );
+};
+
+export const Grid: StoryFn< typeof UseCompositeStorePlaceholder > = (
+ storeProps
+) => {
+ const rtl = isRTL();
+ const store = useCompositeStore( { rtl, ...storeProps } );
+
+ return (
+
+
+ Item A1
+ Item A2
+ Item A3
+
+
+ Item B1
+ Item B2
+ Item B3
+
+
+ Item C1
+ Item C2
+ Item C3
+
+
+ );
+};
+
+export const Hover: StoryFn< typeof UseCompositeStorePlaceholder > = (
+ storeProps
+) => {
+ const rtl = isRTL();
+ const store = useCompositeStore( { rtl, ...storeProps } );
+
+ return (
+
+ }>
+ Hover item one
+
+ }>
+ Hover item two
+
+ }>
+ Hover item three
+
+
+ );
+};
+Hover.parameters = {
+ docs: {
+ description: {
+ story: 'Elements in the composite widget will receive focus on mouse move and lose focus to the composite base element on mouse leave.',
+ },
+ },
+};
+
+export const Typeahead: StoryFn< typeof UseCompositeStorePlaceholder > = (
+ storeProps
+) => {
+ const rtl = isRTL();
+ const store = useCompositeStore( { rtl, ...storeProps } );
+
+ return (
+ }>
+ Apple
+ Banana
+ Peach
+
+ );
+};
+Typeahead.parameters = {
+ docs: {
+ description: {
+ story: 'When focus in on the composite widget, hitting printable character keys will move focus to the next composite item that begins with the input characters.',
+ },
+ },
+};
diff --git a/packages/components/src/composite/current/stories/utils.tsx b/packages/components/src/composite/stories/utils.tsx
similarity index 62%
rename from packages/components/src/composite/current/stories/utils.tsx
rename to packages/components/src/composite/stories/utils.tsx
index 4b2d1bba4b312b..f2f197877ff76d 100644
--- a/packages/components/src/composite/current/stories/utils.tsx
+++ b/packages/components/src/composite/stories/utils.tsx
@@ -6,8 +6,23 @@ import type { StoryContext } from '@storybook/react';
/**
* Internal dependencies
*/
-import type { CompositeStoreProps } from '..';
+import type { CompositeStoreProps } from '../types';
+/**
+ * Renders a widget based on the WAI-ARIA [`composite`](https://w3c.github.io/aria/#composite)
+ * role, which provides a single tab stop on the page and arrow key navigation
+ * through the focusable descendants.
+ *
+ * ```jsx
+ * import { Composite, useCompositeStore } from '@wordpress/components';
+ *
+ * const store = useCompositeStore();
+ *
+ * Item 1
+ * Item 2
+ *
+ * ```
+ */
export function UseCompositeStorePlaceholder( props: CompositeStoreProps ) {
return (
@@ -22,17 +37,17 @@ export function UseCompositeStorePlaceholder( props: CompositeStoreProps ) {
}
UseCompositeStorePlaceholder.displayName = 'useCompositeStore';
+// The output generated by Storybook for these components is
+// messy, so we apply this transform to make it more useful
+// for anyone reading the docs.
export function transform( code: string, context: StoryContext ) {
- // The output generated by Storybook for these components is
- // messy, so we apply this transform to make it more useful
- // for anyone reading the docs.
- const config = ` ${ JSON.stringify( context.args, null, 2 ) } `;
- const state = config.replace( ' {} ', '' );
+ const storeConfig = ` ${ JSON.stringify( context.args, null, 2 ) } `;
+ const formattedStoreConfig = storeConfig.replace( ' {} ', '' );
return [
// Include a setup line, showing how to make use of
// `useCompositeStore` to convert store options into
// a composite store prop.
- `const store = useCompositeStore(${ state });`,
+ `const store = useCompositeStore(${ formattedStoreConfig });`,
'',
'return (',
' ' +
diff --git a/packages/components/src/composite/types.ts b/packages/components/src/composite/types.ts
new file mode 100644
index 00000000000000..05a2b8473eb349
--- /dev/null
+++ b/packages/components/src/composite/types.ts
@@ -0,0 +1,298 @@
+/**
+ * External dependencies
+ */
+import type * as Ariakit from '@ariakit/react';
+
+export type CompositeContextProps =
+ | {
+ /**
+ * Object returned by the `useCompositeStore` hook.
+ */
+ store: Ariakit.CompositeStore;
+ }
+ | undefined;
+
+export type CompositeStoreProps = {
+ /**
+ * The current active item `id`. The active item is the element within the
+ * composite widget that has either DOM or virtual focus (in case
+ * the `virtualFocus` prop is enabled).
+ * - `null` represents the base composite element (the one with a [composite
+ * role](https://w3c.github.io/aria/#composite)). Users will be able to
+ * navigate out of it using arrow keys.
+ * - If `activeId` 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.
+ */
+ activeId?: Ariakit.CompositeStoreProps[ 'activeId' ];
+ /**
+ * The composite item id that should be active by default when the composite
+ * widget is rendered. If `null`, the composite 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.
+ */
+ defaultActiveId?: Ariakit.CompositeStoreProps[ 'defaultActiveId' ];
+ /**
+ * A callback that gets called when the `activeId` state changes.
+ */
+ setActiveId?: Ariakit.CompositeStoreProps[ 'setActiveId' ];
+ /**
+ * Determines how the focus behaves when the user reaches the end of the
+ * composite widget.
+ *
+ * On one-dimensional composite widgets:
+ * - `true` loops from the last item to the first item and vice-versa.
+ * - `horizontal` loops only if `orientation` is `horizontal` or not set.
+ * - `vertical` loops only if `orientation` is `vertical` or not set.
+ * - If `activeId` is initially set to `null`, the composite element will
+ * be focused in between the last and first items.
+ *
+ * On two-dimensional composite widgets (ie. when using `CompositeRow`):
+ * - `true` loops from the last row/column item to the first item in the same
+ * row/column and vice-versa. If it's the last item in the last row, it
+ * moves to the first item in the first row and vice-versa.
+ * - `horizontal` loops only from the last row item to the first item in the
+ * same row.
+ * - `vertical` loops only from the last column item to the first item in the
+ * column row.
+ * - If `activeId` is initially set to `null`, vertical loop will have no
+ * effect as moving down from the last row or up from the first row will
+ * focus on the composite element.
+ * - If `focusWrap` matches the value of `focusLoop`, it'll wrap between the
+ * last item in the last row or column and the first item in the first row or
+ * column and vice-versa.
+ *
+ * @default false
+ */
+ focusLoop?: Ariakit.CompositeStoreProps[ 'focusLoop' ];
+ /**
+ * **Works only on two-dimensional composite widgets**.
+ *
+ * If enabled, moving to the next item from the last one in a row or column
+ * will focus on the first item in the next row or column and vice-versa.
+ * - `true` wraps between rows and columns.
+ * - `horizontal` wraps only between rows.
+ * - `vertical` wraps only between columns.
+ * - If `focusLoop` matches the value of `focusWrap`, it'll wrap between the
+ * last item in the last row or column and the first item in the first row or
+ * column and vice-versa.
+ *
+ * @default false
+ */
+ focusWrap?: Ariakit.CompositeStoreProps[ 'focusWrap' ];
+ /**
+ * **Works only on two-dimensional composite widgets**.
+ *
+ * If enabled, moving up or down when there's no next item or when the next
+ * item is disabled will shift to the item right before it.
+ *
+ * @default false
+ */
+ focusShift?: Ariakit.CompositeStoreProps[ 'focusShift' ];
+ /**
+ * If enabled, the composite element will act as an
+ * [`aria-activedescendant`](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_focus_activedescendant)
+ * container instead of [roving
+ * tabindex](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#kbd_roving_tabindex).
+ * DOM focus will remain on the composite element while its items receive
+ * virtual focus.
+ *
+ * In both scenarios, the item in focus will carry the `data-active-item`
+ * attribute.
+ *
+ * @default false
+ */
+ virtualFocus?: Ariakit.CompositeStoreProps[ 'virtualFocus' ];
+ /**
+ * Defines the orientation of the composite widget. If the composite has a
+ * single row or column (one-dimensional), the `orientation` value 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.
+ *
+ * It doesn't have any effect on two-dimensional composites.
+ *
+ * @default "both"
+ */
+ orientation?: Ariakit.CompositeStoreProps[ 'orientation' ];
+ /**
+ * Determines how the `store`'s `next` and `previous` functions will behave.
+ * If `rtl` is set to `true`, they will be inverted.
+ *
+ * This only affects the composite widget behavior. You still need to set
+ * `dir="rtl"` on HTML/CSS.
+ *
+ * @default false
+ */
+ rtl?: Ariakit.CompositeStoreProps[ 'rtl' ];
+};
+
+export type CompositeProps = {
+ /**
+ * Object returned by the `useCompositeStore` hook.
+ */
+ store: Ariakit.CompositeStore;
+ /**
+ * 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.CompositeProps[ 'render' ];
+ /**
+ * Makes the component a focusable element. When this element gains keyboard
+ * focus, it gets a `data-focus-visible` attribute and triggers the
+ * `onFocusVisible` prop.
+ * The component supports the `disabled` prop even for those elements not
+ * supporting the native `disabled` attribute. Disabled elements may be
+ * still accessible via keyboard by using the the `accessibleWhenDisabled`
+ * prop.
+ * Non-native focusable elements will lose their focusability entirely.
+ * However, native focusable elements will retain their inherent focusability.
+ */
+ focusable?: Ariakit.CompositeProps[ 'focusable' ];
+ /**
+ * 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.
+ *
+ * **Note**: For this prop to work, the `focusable` prop must be set to
+ * `true`, if it's not set by default.
+ *
+ * @default false
+ */
+ disabled?: Ariakit.CompositeProps[ '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.CompositeProps[ 'accessibleWhenDisabled' ];
+ /**
+ * Custom event handler invoked when the element gains focus through keyboard
+ * interaction or a key press occurs while the element is in focus. This is
+ * the programmatic equivalent of the `data-focus-visible` attribute.
+ *
+ * **Note**: For this prop to work, the `focusable` prop must be set to `true`
+ * if it's not set by default.
+ */
+ onFocusVisible?: Ariakit.CompositeProps[ 'onFocusVisible' ];
+ /**
+ * The contents of the component.
+ */
+ children?: Ariakit.CompositeProps[ 'children' ];
+};
+
+export type CompositeGroupProps = {
+ /**
+ * 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.CompositeGroupProps[ 'render' ];
+ /**
+ * The contents of the component.
+ */
+ children?: Ariakit.CompositeGroupProps[ 'children' ];
+};
+
+export type CompositeGroupLabelProps = {
+ /**
+ * 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.CompositeGroupLabelProps[ 'render' ];
+ /**
+ * The contents of the component.
+ */
+ children?: Ariakit.CompositeGroupLabelProps[ 'children' ];
+};
+
+export type CompositeItemProps = {
+ /**
+ * 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.CompositeItemProps[ 'render' ];
+ /**
+ * The contents of the component.
+ */
+ children?: Ariakit.CompositeItemProps[ 'children' ];
+ /**
+ * 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.CompositeItemProps[ 'accessibleWhenDisabled' ];
+};
+
+export type CompositeRowProps = {
+ /**
+ * 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.CompositeRowProps[ 'render' ];
+ /**
+ * The contents of the component.
+ */
+ children?: Ariakit.CompositeRowProps[ 'children' ];
+};
+
+export type CompositeHoverProps = {
+ /**
+ * 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.CompositeHoverProps[ 'render' ];
+ /**
+ * The contents of the component.
+ */
+ children?: Ariakit.CompositeHoverProps[ 'children' ];
+};
+
+export type CompositeTypeaheadProps = {
+ /**
+ * 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.CompositeTypeaheadProps[ 'render' ];
+ /**
+ * The contents of the component.
+ */
+ children?: Ariakit.CompositeTypeaheadProps[ 'children' ];
+};
diff --git a/packages/components/src/composite/v2.ts b/packages/components/src/composite/v2.ts
deleted file mode 100644
index 38d3f628d368b6..00000000000000
--- a/packages/components/src/composite/v2.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Although we have migrated away from Reakit, the 'current'
-// Ariakit implementation is still considered a v2.
-
-export * from './current';
diff --git a/packages/components/src/custom-gradient-picker/style.scss b/packages/components/src/custom-gradient-picker/style.scss
index e7828127ff8d33..a0b616d6ca1d72 100644
--- a/packages/components/src/custom-gradient-picker/style.scss
+++ b/packages/components/src/custom-gradient-picker/style.scss
@@ -1,7 +1,7 @@
$components-custom-gradient-picker__padding: $grid-unit-20; // 48px container, 16px handles inside, that leaves 32px padding, half of which is 1å6.
.components-custom-gradient-picker__gradient-bar {
- border-radius: $radius-block-ui;
+ border-radius: $radius-small;
width: 100%;
height: $grid-unit-60;
position: relative;
@@ -112,7 +112,7 @@ $components-custom-gradient-picker__padding: $grid-unit-20; // 48px container, 1
&.is-pressed {
> svg {
background: $white;
- border: 1px solid $gray-600;
+ border: $border-width solid $gray-600;
border-radius: 2px;
}
}
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 e925038812fa73..bb458abcc282ff 100644
--- a/packages/components/src/custom-select-control-v2/custom-select.tsx
+++ b/packages/components/src/custom-select-control-v2/custom-select.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import * as Ariakit from '@ariakit/react';
+import { useStoreState } from '@ariakit/react';
/**
* WordPress dependencies
@@ -62,7 +63,7 @@ const CustomSelectButton = ( {
CustomSelectStore,
'onChange'
> ) => {
- const { value: currentValue } = store.useState();
+ const { value: currentValue } = useStoreState( store );
const computedRenderSelectedValue = useMemo(
() => renderSelectedValue ?? defaultRenderSelectedValue,
diff --git a/packages/components/src/custom-select-control/README.md b/packages/components/src/custom-select-control/README.md
index 6804f4cf6ecd3a..a764a0df133eab 100644
--- a/packages/components/src/custom-select-control/README.md
+++ b/packages/components/src/custom-select-control/README.md
@@ -142,6 +142,13 @@ A handler for `blur` events on the trigger button.
- Required: No
+#### `__next40pxDefaultSize`: `boolean`
+
+Start opting into the larger default height that will become the default size in a future version.
+
+- Required: No
+- Default: `false`
+
## Related components
- Like this component, but implemented using a native `
-
+ { !! help && (
+
+ { help }
+
+ ) }
+
);
}
diff --git a/packages/components/src/radio-control/style.scss b/packages/components/src/radio-control/style.scss
index 7444ea1343b842..e3dcb8d1e58270 100644
--- a/packages/components/src/radio-control/style.scss
+++ b/packages/components/src/radio-control/style.scss
@@ -1,3 +1,12 @@
+.components-radio-control {
+ border: 0;
+ margin: 0;
+ padding: 0;
+
+ font-family: $default-font;
+ font-size: $default-font-size;
+}
+
.components-radio-control__group-wrapper.has-help {
margin-block-end: $grid-unit-15;
}
@@ -32,7 +41,7 @@
&::before {
content: "";
- border-radius: 50%;
+ border-radius: $radius-round;
}
}
}
@@ -57,5 +66,7 @@
// Override the top margin of the StyledHelp component from BaseControl.
// TODO: we should tweak the StyledHelp component to not have a top margin.
- margin-top: 0;
+ &.components-radio-control__option-description {
+ margin-top: 0;
+ }
}
diff --git a/packages/components/src/radio-control/test/index.tsx b/packages/components/src/radio-control/test/index.tsx
index 0be166513a9a4d..7e30744d96b826 100644
--- a/packages/components/src/radio-control/test/index.tsx
+++ b/packages/components/src/radio-control/test/index.tsx
@@ -56,6 +56,47 @@ describe.each( [
const [ , Component ] = modeAndComponent;
describe( 'semantics and labelling', () => {
+ it( 'should group all radios under a fieldset with an accessible label (legend)', () => {
+ const onChangeSpy = jest.fn();
+ render(
+
+ );
+
+ expect(
+ screen.getByRole( 'group', { name: defaultProps.label } )
+ ).toBeVisible();
+ } );
+
+ it( 'should group all radios under a fieldset with an accessible label even when the label is visually hidden', () => {
+ const onChangeSpy = jest.fn();
+ render(
+
+ );
+
+ expect(
+ screen.getByRole( 'group', { name: defaultProps.label } )
+ ).toBeVisible();
+ } );
+
+ it( 'should describe the radio group with the help text', () => {
+ const onChangeSpy = jest.fn();
+ render(
+
+ );
+
+ expect(
+ screen.getByRole( 'group', { name: defaultProps.label } )
+ ).toHaveAccessibleDescription( 'Test help text' );
+ } );
+
it( 'should render radio inputs with accessible labels', () => {
const onChangeSpy = jest.fn();
render(
@@ -101,46 +142,7 @@ describe.each( [
).toHaveAccessibleName( defaultProps.options[ 1 ].label );
} );
- it( 'should use the group help text to describe individual options', () => {
- const onChangeSpy = jest.fn();
- render(
-
- );
-
- for ( const option of defaultProps.options ) {
- expect(
- screen.getByRole( 'radio', { name: option.label } )
- ).toHaveAccessibleDescription( 'Select your favorite animal.' );
- }
- } );
-
it( 'should use the option description text to describe individual options', () => {
- const onChangeSpy = jest.fn();
- render(
-
- );
-
- let index = 1;
- for ( const option of defaultProps.options ) {
- expect(
- screen.getByRole( 'radio', { name: option.label } )
- ).toHaveAccessibleDescription(
- `This is the option number ${ index }.`
- );
- index += 1;
- }
- } );
-
- it( 'should use both the option description text and the group help text to describe individual options', () => {
const onChangeSpy = jest.fn();
render(
);
+ // Group help text should not be used to describe individual options.
let index = 1;
for ( const option of defaultProps.options ) {
expect(
screen.getByRole( 'radio', { name: option.label } )
).toHaveAccessibleDescription(
- `This is the option number ${ index }. Select your favorite animal`
+ `This is the option number ${ index }.`
);
index += 1;
}
diff --git a/packages/components/src/radio-group/radio.tsx b/packages/components/src/radio-group/radio.tsx
index 0c90c337fcdf2f..50a5a2647b39d5 100644
--- a/packages/components/src/radio-group/radio.tsx
+++ b/packages/components/src/radio-group/radio.tsx
@@ -7,6 +7,7 @@ import { forwardRef, useContext } from '@wordpress/element';
* External dependencies
*/
import * as Ariakit from '@ariakit/react';
+import { useStoreState } from '@ariakit/react';
/**
* Internal dependencies
@@ -26,7 +27,7 @@ function UnforwardedRadio(
) {
const { store, disabled } = useContext( RadioGroupContext );
- const selectedValue = store?.useState( 'value' );
+ const selectedValue = useStoreState( store, 'value' );
const isChecked = selectedValue !== undefined && selectedValue === value;
return (
diff --git a/packages/components/src/range-control/README.md b/packages/components/src/range-control/README.md
index 3da335b9ecf6e8..cfa8c76740e74f 100644
--- a/packages/components/src/range-control/README.md
+++ b/packages/components/src/range-control/README.md
@@ -362,6 +362,13 @@ Determines if the `input` number field will render next to the RangeControl. Thi
- Required: No
- Platform: Web
+### `__next40pxDefaultSize`: `boolean`
+
+Start opting into the larger default height that will become the default size in a future version.
+
+- Required: No
+- Default: `false`
+
### `__nextHasNoMarginBottom`: `boolean`
Start opting into the new margin-free styles that will become the default in a future version.
diff --git a/packages/components/src/range-control/index.tsx b/packages/components/src/range-control/index.tsx
index 5b4ecfa585679b..c9fbdc0055c855 100644
--- a/packages/components/src/range-control/index.tsx
+++ b/packages/components/src/range-control/index.tsx
@@ -41,6 +41,25 @@ import { space } from '../utils/space';
const noop = () => {};
+/**
+ * Computes the value that `RangeControl` should reset to when pressing
+ * the reset button.
+ */
+function computeResetValue( {
+ resetFallbackValue,
+ initialPosition,
+}: Pick< RangeControlProps, 'resetFallbackValue' | 'initialPosition' > ) {
+ if ( resetFallbackValue !== undefined ) {
+ return ! Number.isNaN( resetFallbackValue ) ? resetFallbackValue : null;
+ }
+
+ if ( initialPosition !== undefined ) {
+ return ! Number.isNaN( initialPosition ) ? initialPosition : null;
+ }
+
+ return null;
+}
+
function UnforwardedRangeControl(
props: WordPressComponentProps< RangeControlProps, 'input', false >,
forwardedRef: ForwardedRef< HTMLInputElement >
@@ -166,13 +185,12 @@ function UnforwardedRangeControl(
};
const handleOnReset = () => {
- let resetValue: number | null = parseFloat( `${ resetFallbackValue }` );
- let onChangeResetValue: number | undefined = resetValue;
-
- if ( isNaN( resetValue ) ) {
- resetValue = null;
- onChangeResetValue = undefined;
- }
+ // Reset to `resetFallbackValue` if defined, otherwise set internal value
+ // to `null` — which, if propagated to the `value` prop, will cause
+ // the value to be reset to the `initialPosition` prop if defined.
+ const resetValue = Number.isNaN( resetFallbackValue )
+ ? null
+ : resetFallbackValue ?? null;
setValue( resetValue );
@@ -189,7 +207,7 @@ function UnforwardedRangeControl(
* preserve the undefined callback argument, except when a
* resetFallbackValue is defined.
*/
- onChange( onChangeResetValue );
+ onChange( resetValue ?? undefined );
};
const handleShowTooltip = () => setShowTooltip( true );
@@ -210,9 +228,11 @@ function UnforwardedRangeControl(
const offsetStyle = {
[ isRTL() ? 'right' : 'left' ]: fillValueOffset,
};
+
return (
= ( { onChange, ...args } ) => {
export const Default: StoryFn< typeof RangeControl > = Template.bind( {} );
Default.args = {
+ __nextHasNoMarginBottom: true,
help: 'Please select how transparent you would like this.',
initialPosition: 50,
label: 'Opacity',
@@ -104,6 +105,7 @@ export const WithAnyStep: StoryFn< typeof RangeControl > = ( {
);
};
WithAnyStep.args = {
+ __nextHasNoMarginBottom: true,
label: 'Brightness',
step: 'any',
};
@@ -167,6 +169,7 @@ export const WithIntegerStepAndMarks: StoryFn< typeof RangeControl > =
MarkTemplate.bind( {} );
WithIntegerStepAndMarks.args = {
+ __nextHasNoMarginBottom: true,
label: 'Integer Step',
marks: marksBase,
max: 10,
@@ -183,6 +186,7 @@ export const WithDecimalStepAndMarks: StoryFn< typeof RangeControl > =
MarkTemplate.bind( {} );
WithDecimalStepAndMarks.args = {
+ __nextHasNoMarginBottom: true,
marks: [
...marksBase,
{ value: 3.5, label: '3.5' },
@@ -202,6 +206,7 @@ export const WithNegativeMinimumAndMarks: StoryFn< typeof RangeControl > =
MarkTemplate.bind( {} );
WithNegativeMinimumAndMarks.args = {
+ __nextHasNoMarginBottom: true,
marks: marksWithNegatives,
max: 10,
min: -10,
@@ -217,6 +222,7 @@ export const WithNegativeRangeAndMarks: StoryFn< typeof RangeControl > =
MarkTemplate.bind( {} );
WithNegativeRangeAndMarks.args = {
+ __nextHasNoMarginBottom: true,
marks: marksWithNegatives,
max: -1,
min: -10,
@@ -232,6 +238,7 @@ export const WithAnyStepAndMarks: StoryFn< typeof RangeControl > =
MarkTemplate.bind( {} );
WithAnyStepAndMarks.args = {
+ __nextHasNoMarginBottom: true,
marks: marksBase,
max: 10,
min: 0,
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 89f4864aee2ea6..ec1572d2679247 100644
--- a/packages/components/src/range-control/styles/range-control-styles.ts
+++ b/packages/components/src/range-control/styles/range-control-styles.ts
@@ -154,7 +154,7 @@ export const Mark = styled.span`
height: ${ thumbSize }px;
left: 0;
position: absolute;
- top: -4px;
+ top: 9px;
width: 1px;
${ markFill };
@@ -170,7 +170,7 @@ export const MarkLabel = styled.span`
color: ${ COLORS.gray[ 300 ] };
font-size: 11px;
position: absolute;
- top: 12px;
+ top: 22px;
white-space: nowrap;
${ rtl( { left: 0 } ) };
diff --git a/packages/components/src/range-control/test/index.tsx b/packages/components/src/range-control/test/index.tsx
index d843b615ed0078..3ce741867d0dbc 100644
--- a/packages/components/src/range-control/test/index.tsx
+++ b/packages/components/src/range-control/test/index.tsx
@@ -6,7 +6,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
/**
* Internal dependencies
*/
-import RangeControl from '../';
+import _RangeControl from '../';
const getRangeInput = (): HTMLInputElement => screen.getByRole( 'slider' );
const getNumberInput = (): HTMLInputElement => screen.getByRole( 'spinbutton' );
@@ -15,6 +15,12 @@ const getResetButton = (): HTMLButtonElement => screen.getByRole( 'button' );
const fireChangeEvent = ( input: HTMLInputElement, value?: number | string ) =>
fireEvent.change( input, { target: { value } } );
+const RangeControl = (
+ props: React.ComponentProps< typeof _RangeControl >
+) => {
+ return <_RangeControl { ...props } __nextHasNoMarginBottom />;
+};
+
describe( 'RangeControl', () => {
describe( '#render()', () => {
it( 'should trigger change callback with numeric value', () => {
@@ -292,14 +298,37 @@ describe( 'RangeControl', () => {
} );
describe( 'reset', () => {
- it( 'should reset to a custom fallback value, defined by a parent component', () => {
+ it( 'should clear the input value when clicking the reset button', () => {
+ const spy = jest.fn();
+ render( );
+
+ const resetButton = getResetButton();
+ const rangeInput = getRangeInput();
+ const numberInput = getNumberInput();
+
+ fireChangeEvent( numberInput, '14' );
+
+ expect( rangeInput.value ).toBe( '14' );
+ expect( numberInput.value ).toBe( '14' );
+ expect( spy ).toHaveBeenCalledWith( 14 );
+
+ fireEvent.click( resetButton );
+
+ // range input resets to min + (max-min)/2
+ expect( rangeInput.value ).toBe( '50' );
+ expect( numberInput.value ).toBe( '' );
+ expect( spy ).toHaveBeenCalledWith( undefined );
+
+ expect( resetButton ).toHaveAttribute( 'aria-disabled', 'true' );
+ } );
+
+ it( 'should reset to the `initialPosition` value when clicking the reset button', () => {
const spy = jest.fn();
render(
);
@@ -307,21 +336,29 @@ describe( 'RangeControl', () => {
const rangeInput = getRangeInput();
const numberInput = getNumberInput();
+ fireChangeEvent( numberInput, '14' );
+
+ expect( rangeInput.value ).toBe( '14' );
+ expect( numberInput.value ).toBe( '14' );
+ expect( spy ).toHaveBeenCalledWith( 14 );
+
fireEvent.click( resetButton );
- expect( rangeInput.value ).toBe( '33' );
- expect( numberInput.value ).toBe( '33' );
- expect( spy ).toHaveBeenCalledWith( 33 );
+ expect( rangeInput.value ).toBe( '23' );
+ expect( numberInput.value ).toBe( '23' );
+ expect( spy ).toHaveBeenCalledWith( undefined );
+
+ expect( resetButton ).toHaveAttribute( 'aria-disabled', 'true' );
} );
- it( 'should reset to a 50% of min/max value, of no initialPosition or value is defined', () => {
+ it( 'should reset to the `resetFallbackValue` value when clicking the reset button', () => {
+ const spy = jest.fn();
render(
);
@@ -331,8 +368,11 @@ describe( 'RangeControl', () => {
fireEvent.click( resetButton );
- expect( rangeInput.value ).toBe( '50' );
- expect( numberInput.value ).toBe( '' );
+ expect( rangeInput.value ).toBe( '33' );
+ expect( numberInput.value ).toBe( '33' );
+ expect( spy ).toHaveBeenCalledWith( 33 );
+
+ expect( resetButton ).toHaveAttribute( 'aria-disabled', 'true' );
} );
} );
} );
diff --git a/packages/components/src/search-control/index.tsx b/packages/components/src/search-control/index.tsx
index 08cb3b065c904e..aac905e137e025 100644
--- a/packages/components/src/search-control/index.tsx
+++ b/packages/components/src/search-control/index.tsx
@@ -77,10 +77,13 @@ function UnforwardedSearchControl(
const contextValue = useMemo(
() => ( {
- // Overrides the underlying BaseControl `__nextHasNoMarginBottom` via the context system
- // to provide backwards compatibile margin for SearchControl.
- // (In a standard InputControl, the BaseControl `__nextHasNoMarginBottom` is always set to true.)
- BaseControl: { _overrides: { __nextHasNoMarginBottom } },
+ BaseControl: {
+ // Overrides the underlying BaseControl `__nextHasNoMarginBottom` via the context system
+ // to provide backwards compatibile margin for SearchControl.
+ // (In a standard InputControl, the BaseControl `__nextHasNoMarginBottom` is always set to true.)
+ _overrides: { __nextHasNoMarginBottom },
+ __associatedWPComponentName: 'SearchControl',
+ },
// `isBorderless` is still experimental and not a public prop for InputControl yet.
InputBase: { isBorderless: true },
} ),
diff --git a/packages/components/src/search-control/stories/index.story.tsx b/packages/components/src/search-control/stories/index.story.tsx
index 433d3eef655adf..215288bb67c9b6 100644
--- a/packages/components/src/search-control/stories/index.story.tsx
+++ b/packages/components/src/search-control/stories/index.story.tsx
@@ -48,6 +48,7 @@ const Template: StoryFn< typeof SearchControl > = ( {
export const Default = Template.bind( {} );
Default.args = {
help: 'Help text to explain the input.',
+ __nextHasNoMarginBottom: true,
};
/**
diff --git a/packages/components/src/search-control/test/index.tsx b/packages/components/src/search-control/test/index.tsx
index f130cab1b2a7cd..c6637945adcf63 100644
--- a/packages/components/src/search-control/test/index.tsx
+++ b/packages/components/src/search-control/test/index.tsx
@@ -23,6 +23,7 @@ function ControlledSearchControl( {
return (
{
setValue( ...args );
diff --git a/packages/components/src/select-control/README.md b/packages/components/src/select-control/README.md
index 464ee180b38639..6c3ecda886a188 100644
--- a/packages/components/src/select-control/README.md
+++ b/packages/components/src/select-control/README.md
@@ -229,6 +229,14 @@ The style variant of the control.
- Required: No
- Default: `'default'`
+### __next40pxDefaultSize
+
+Start opting into the larger default height that will become the default size in a future version.
+
+- Type: `Boolean`
+- Required: No
+- Default: `false`
+
### __nextHasNoMarginBottom
Start opting into the new margin-free styles that will become the default in a future version.
diff --git a/packages/components/src/select-control/index.tsx b/packages/components/src/select-control/index.tsx
index ca9966fc675b86..3686661b8a58dc 100644
--- a/packages/components/src/select-control/index.tsx
+++ b/packages/components/src/select-control/index.tsx
@@ -99,6 +99,7 @@ function UnforwardedSelectControl< V extends string >(
help={ help }
id={ id }
__nextHasNoMarginBottom={ __nextHasNoMarginBottom }
+ __associatedWPComponentName="SelectControl"
>
= ( props ) => {
export const Default = SelectControlWithState.bind( {} );
Default.args = {
+ __nextHasNoMarginBottom: true,
options: [
{ value: '', label: 'Select an Option', disabled: true },
{ value: 'a', label: 'Option A' },
@@ -82,9 +83,11 @@ WithLabelAndHelpText.args = {
* As an alternative to the `options` prop, `optgroup`s and `options` can be
* passed in as `children` for more customizeability.
*/
-export const WithCustomChildren: StoryFn< typeof SelectControl > = ( args ) => {
- return (
-
+export const WithCustomChildren = SelectControlWithState.bind( {} );
+WithCustomChildren.args = {
+ __nextHasNoMarginBottom: true,
+ children: (
+ <>
-
- );
+ >
+ ),
};
export const Minimal = SelectControlWithState.bind( {} );
diff --git a/packages/components/src/select-control/test/select-control.tsx b/packages/components/src/select-control/test/select-control.tsx
index 0e8a6891087043..47b684cd20e280 100644
--- a/packages/components/src/select-control/test/select-control.tsx
+++ b/packages/components/src/select-control/test/select-control.tsx
@@ -7,7 +7,13 @@ import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
-import SelectControl from '..';
+import _SelectControl from '..';
+
+const SelectControl = (
+ props: React.ComponentProps< typeof _SelectControl >
+) => {
+ return <_SelectControl { ...props } __nextHasNoMarginBottom />;
+};
describe( 'SelectControl', () => {
it( 'should not render when no options or children are provided', () => {
@@ -123,7 +129,7 @@ describe( 'SelectControl', () => {
onChange={ onChange }
/>;
- {
} );
it( 'should accept an explicit type argument', () => {
-
+ <_SelectControl< 'narrow' | 'value' >
// @ts-expect-error "string" is not "narrow" or "value"
value="string"
options={ [
@@ -166,7 +172,7 @@ describe( 'SelectControl', () => {
value: ( 'foo' | 'bar' )[]
) => void = () => {};
- {
onChange={ onChange }
/>;
- {
} );
it( 'should accept an explicit type argument', () => {
-
+ <_SelectControl< 'narrow' | 'value' >
multiple
// @ts-expect-error "string" is not "narrow" or "value"
value={ [ 'string' ] }
diff --git a/packages/components/src/snackbar/style.scss b/packages/components/src/snackbar/style.scss
index 0ba1774d67382f..e7f29267d6f6db 100644
--- a/packages/components/src/snackbar/style.scss
+++ b/packages/components/src/snackbar/style.scss
@@ -3,8 +3,8 @@
font-size: $default-font-size;
background: rgba($black, 0.85); // Emulates #1e1e1e closely.
backdrop-filter: blur($grid-unit-20) saturate(180%);
- border-radius: $radius-block-ui;
- box-shadow: $shadow-popover;
+ border-radius: $radius-medium;
+ box-shadow: $elevation-x-small;
color: $white;
padding: $grid-unit-15 ($grid-unit-05 * 5);
width: 100%;
diff --git a/packages/components/src/tab-panel/index.tsx b/packages/components/src/tab-panel/index.tsx
index f85119d938c659..9e180c30321b93 100644
--- a/packages/components/src/tab-panel/index.tsx
+++ b/packages/components/src/tab-panel/index.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import * as Ariakit from '@ariakit/react';
+import { useStoreState } from '@ariakit/react';
import clsx from 'clsx';
import type { ForwardedRef } from 'react';
@@ -121,7 +122,9 @@ const UnforwardedTabPanel = (
defaultSelectedId: prependInstanceId( initialTabName ),
} );
- const selectedTabName = extractTabName( tabStore.useState( 'selectedId' ) );
+ const selectedTabName = extractTabName(
+ useStoreState( tabStore, 'selectedId' )
+ );
const setTabStoreSelectedId = useCallback(
( tabName: string ) => {
diff --git a/packages/components/src/tab-panel/style.scss b/packages/components/src/tab-panel/style.scss
index 2855f8c2b06a01..ab73a7affaeed4 100644
--- a/packages/components/src/tab-panel/style.scss
+++ b/packages/components/src/tab-panel/style.scss
@@ -67,7 +67,7 @@
// Draw the indicator.
box-shadow: 0 0 0 0 transparent;
- border-radius: $radius-block-ui;
+ border-radius: $radius-small;
// Animation
transition: all 0.1s linear;
diff --git a/packages/components/src/tabs/index.tsx b/packages/components/src/tabs/index.tsx
index cad4a217efd44d..ef8978b647a904 100644
--- a/packages/components/src/tabs/index.tsx
+++ b/packages/components/src/tabs/index.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import * as Ariakit from '@ariakit/react';
+import { useStoreState } from '@ariakit/react';
/**
* WordPress dependencies
@@ -48,7 +49,7 @@ function Tabs( {
const isControlled = selectedTabId !== undefined;
- const { items, selectedId, activeId } = store.useState();
+ const { items, selectedId, activeId } = useStoreState( store );
const { setSelectedId, setActiveId } = store;
// Keep track of whether tabs have been populated. This is used to prevent
diff --git a/packages/components/src/tabs/tablist.tsx b/packages/components/src/tabs/tablist.tsx
index 4906e0088417b7..80ed9b4c5bea2f 100644
--- a/packages/components/src/tabs/tablist.tsx
+++ b/packages/components/src/tabs/tablist.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import * as Ariakit from '@ariakit/react';
+import { useStoreState } from '@ariakit/react';
/**
* WordPress dependencies
@@ -26,7 +27,8 @@ export const TabList = forwardRef<
>( function TabList( { children, ...otherProps }, ref ) {
const context = useTabsContext();
- const selectedId = context?.store.useState( 'selectedId' );
+ const tabStoreState = useStoreState( context?.store );
+ const selectedId = tabStoreState?.selectedId;
const indicatorPosition = useTrackElementOffsetRect(
context?.store.item( selectedId )?.element
);
@@ -37,13 +39,13 @@ export const TabList = forwardRef<
( { previousValue } ) => previousValue && setAnimationEnabled( true )
);
- if ( ! context ) {
+ if ( ! context || ! tabStoreState ) {
warning( '`Tabs.TabList` must be wrapped in a `Tabs` component.' );
return null;
}
- const { store } = context;
- const { activeId, selectOnMove } = store.useState();
+ const { store } = context;
+ const { activeId, selectOnMove } = tabStoreState;
const { setActiveId } = store;
const onBlur = () => {
diff --git a/packages/components/src/tabs/tabpanel.tsx b/packages/components/src/tabs/tabpanel.tsx
index 439671a39ff9b7..512b2609682722 100644
--- a/packages/components/src/tabs/tabpanel.tsx
+++ b/packages/components/src/tabs/tabpanel.tsx
@@ -1,7 +1,11 @@
/**
- * WordPress dependencies
+ * External dependencies
*/
+import { useStoreState } from '@ariakit/react';
+/**
+ * WordPress dependencies
+ */
import { forwardRef } from '@wordpress/element';
/**
@@ -22,13 +26,13 @@ export const TabPanel = forwardRef<
ref
) {
const context = useTabsContext();
+ const selectedId = useStoreState( context?.store, 'selectedId' );
if ( ! context ) {
warning( '`Tabs.TabPanel` must be wrapped in a `Tabs` component.' );
return null;
}
const { store, instanceId } = context;
const instancedTabId = `${ instanceId }-${ tabId }`;
- const selectedId = store.useState( ( state ) => state.selectedId );
return (
= ( {
export const Default: StoryFn< typeof TextControl > = DefaultTemplate.bind(
{}
);
-Default.args = {};
+Default.args = {
+ __nextHasNoMarginBottom: true,
+};
export const WithLabelAndHelpText: StoryFn< typeof TextControl > =
DefaultTemplate.bind( {} );
diff --git a/packages/components/src/text-control/test/text-control.tsx b/packages/components/src/text-control/test/text-control.tsx
index fc048b93992f08..19b17cae443614 100644
--- a/packages/components/src/text-control/test/text-control.tsx
+++ b/packages/components/src/text-control/test/text-control.tsx
@@ -6,7 +6,11 @@ import { render, screen } from '@testing-library/react';
/**
* Internal dependencies
*/
-import TextControl from '..';
+import _TextControl from '..';
+
+const TextControl = ( props: React.ComponentProps< typeof _TextControl > ) => {
+ return <_TextControl { ...props } __nextHasNoMarginBottom />;
+};
const noop = () => {};
diff --git a/packages/components/src/text/styles.ts b/packages/components/src/text/styles.ts
index c7d48552795938..e777ed4f0941de 100644
--- a/packages/components/src/text/styles.ts
+++ b/packages/components/src/text/styles.ts
@@ -35,7 +35,7 @@ export const muted = css`
export const highlighterText = css`
mark {
background: ${ COLORS.alert.yellow };
- border-radius: 2px;
+ border-radius: ${ CONFIG.radiusSmall };
box-shadow:
0 0 0 1px rgba( 0, 0, 0, 0.05 ) inset,
0 -1px 0 rgba( 0, 0, 0, 0.1 ) inset;
diff --git a/packages/components/src/textarea-control/index.tsx b/packages/components/src/textarea-control/index.tsx
index 3b96e11b0621b5..e7528510667b75 100644
--- a/packages/components/src/textarea-control/index.tsx
+++ b/packages/components/src/textarea-control/index.tsx
@@ -35,6 +35,7 @@ function UnforwardedTextareaControl(
return (
= ( {
export const Default: StoryFn< typeof TextareaControl > = Template.bind( {} );
Default.args = {
+ __nextHasNoMarginBottom: true,
label: 'Text',
help: 'Enter some text',
+ placeholder: 'Placeholder',
};
diff --git a/packages/components/src/textarea-control/styles/textarea-control-styles.ts b/packages/components/src/textarea-control/styles/textarea-control-styles.ts
index 046e17816eff24..022ce3560ea3a5 100644
--- a/packages/components/src/textarea-control/styles/textarea-control-styles.ts
+++ b/packages/components/src/textarea-control/styles/textarea-control-styles.ts
@@ -2,13 +2,86 @@
* External dependencies
*/
import styled from '@emotion/styled';
+import { css } from '@emotion/react';
/**
* Internal dependencies
*/
-import { inputControl } from '../../utils/input';
+import { font } from '../../utils/font';
+import { COLORS } from '../../utils/colors-values';
+import { CONFIG } from '../../utils';
+import { breakpoint } from '../../utils/breakpoint';
+
+const inputStyleNeutral = css`
+ box-shadow: 0 0 0 transparent;
+ border-radius: ${ CONFIG.radiusSmall };
+ border: ${ CONFIG.borderWidth } solid ${ COLORS.ui.border };
+
+ @media not ( prefers-reduced-motion ) {
+ transition: box-shadow 0.1s linear;
+ }
+`;
+
+const inputStyleFocus = css`
+ border-color: ${ COLORS.theme.accent };
+ box-shadow: 0 0 0
+ calc( ${ CONFIG.borderWidthFocus } - ${ CONFIG.borderWidth } )
+ ${ COLORS.theme.accent };
+
+ // Windows High Contrast mode will show this outline, but not the box-shadow.
+ outline: 2px solid transparent;
+`;
export const StyledTextarea = styled.textarea`
width: 100%;
- ${ inputControl }
+ display: block;
+ font-family: ${ font( 'default.fontFamily' ) };
+ line-height: 20px;
+
+ // Vertical padding is to match the standard 40px control height when rows=1,
+ // in conjunction with the 20px line-height.
+ // "Standard" metrics are 10px 12px, but subtracts 1px each to account for the border width.
+ padding: 9px 11px;
+
+ ${ inputStyleNeutral };
+
+ /* Fonts smaller than 16px causes mobile safari to zoom. */
+ font-size: ${ font( 'mobileTextMinFontSize' ) };
+
+ ${ breakpoint( 'small' ) } {
+ font-size: ${ font( 'default.fontSize' ) };
+ }
+
+ &:focus {
+ ${ inputStyleFocus }
+ }
+
+ // Use opacity to work in various editor styles.
+ &::-webkit-input-placeholder {
+ color: ${ COLORS.ui.darkGrayPlaceholder };
+ }
+
+ &::-moz-placeholder {
+ opacity: 1; // Necessary because Firefox reduces this from 1.
+ color: ${ COLORS.ui.darkGrayPlaceholder };
+ }
+
+ &:-ms-input-placeholder {
+ color: ${ COLORS.ui.darkGrayPlaceholder };
+ }
+
+ .is-dark-theme & {
+ &::-webkit-input-placeholder {
+ color: ${ COLORS.ui.lightGrayPlaceholder };
+ }
+
+ &::-moz-placeholder {
+ opacity: 1; // Necessary because Firefox reduces this from 1.
+ color: ${ COLORS.ui.lightGrayPlaceholder };
+ }
+
+ &:-ms-input-placeholder {
+ color: ${ COLORS.ui.lightGrayPlaceholder };
+ }
+ }
`;
diff --git a/packages/components/src/toggle-control/index.tsx b/packages/components/src/toggle-control/index.tsx
index 5c64d57d3d0249..d2ee234a9695f8 100644
--- a/packages/components/src/toggle-control/index.tsx
+++ b/packages/components/src/toggle-control/index.tsx
@@ -10,6 +10,7 @@ import clsx from 'clsx';
*/
import { forwardRef } from '@wordpress/element';
import { useInstanceId } from '@wordpress/compose';
+import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
@@ -48,6 +49,14 @@ function UnforwardedToggleControl(
! __nextHasNoMarginBottom && css( { marginBottom: space( 3 ) } )
);
+ if ( ! __nextHasNoMarginBottom ) {
+ deprecated( 'Bottom margin styles for wp.components.ToggleControl', {
+ since: '6.7',
+ version: '7.0',
+ hint: 'Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version.',
+ } );
+ }
+
let describedBy, helpLabel;
if ( help ) {
if ( typeof help === 'function' ) {
diff --git a/packages/components/src/toggle-control/stories/index.story.tsx b/packages/components/src/toggle-control/stories/index.story.tsx
index b8043b8f48e523..97723aa207a394 100644
--- a/packages/components/src/toggle-control/stories/index.story.tsx
+++ b/packages/components/src/toggle-control/stories/index.story.tsx
@@ -48,6 +48,7 @@ const Template: StoryFn< typeof ToggleControl > = ( {
export const Default = Template.bind( {} );
Default.args = {
+ __nextHasNoMarginBottom: true,
label: 'Enable something',
};
diff --git a/packages/components/src/toggle-control/test/index.tsx b/packages/components/src/toggle-control/test/index.tsx
index cc89031d9affa3..b0eec2aca6663d 100644
--- a/packages/components/src/toggle-control/test/index.tsx
+++ b/packages/components/src/toggle-control/test/index.tsx
@@ -6,7 +6,13 @@ import { render, screen } from '@testing-library/react';
/**
* Internal dependencies
*/
-import ToggleControl from '..';
+import _ToggleControl from '..';
+
+const ToggleControl = (
+ props: React.ComponentProps< typeof _ToggleControl >
+) => {
+ return <_ToggleControl { ...props } __nextHasNoMarginBottom />;
+};
describe( 'ToggleControl', () => {
it( 'should label the toggle', () => {
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 81afc7ac67b05f..e9b4f4ca22ab85 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
@@ -13,10 +13,6 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] =
box-sizing: inherit;
}
-.emotion-2 {
- margin-bottom: calc(4px * 2);
-}
-
.components-panel__row .emotion-2 {
margin-bottom: inherit;
}
@@ -88,7 +84,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] =
appearance: none;
background: transparent;
border: none;
- border-radius: 2px;
+ border-radius: 1px;
color: #757575;
fill: currentColor;
cursor: pointer;
@@ -156,7 +152,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] =
.emotion-15 {
background: #1e1e1e;
- border-radius: 2px;
+ border-radius: 1px;
position: absolute;
inset: 0;
z-index: 1;
@@ -175,7 +171,7 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] =
appearance: none;
background: transparent;
border: none;
- border-radius: 2px;
+ border-radius: 1px;
color: #757575;
fill: currentColor;
cursor: pointer;
@@ -349,10 +345,6 @@ exports[`ToggleGroupControl controlled should render correctly with text options
box-sizing: inherit;
}
-.emotion-2 {
- margin-bottom: calc(4px * 2);
-}
-
.components-panel__row .emotion-2 {
margin-bottom: inherit;
}
@@ -424,7 +416,7 @@ exports[`ToggleGroupControl controlled should render correctly with text options
appearance: none;
background: transparent;
border: none;
- border-radius: 2px;
+ border-radius: 1px;
color: #757575;
fill: currentColor;
cursor: pointer;
@@ -573,10 +565,6 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`]
box-sizing: inherit;
}
-.emotion-2 {
- margin-bottom: calc(4px * 2);
-}
-
.components-panel__row .emotion-2 {
margin-bottom: inherit;
}
@@ -648,7 +636,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`]
appearance: none;
background: transparent;
border: none;
- border-radius: 2px;
+ border-radius: 1px;
color: #757575;
fill: currentColor;
cursor: pointer;
@@ -716,7 +704,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`]
.emotion-15 {
background: #1e1e1e;
- border-radius: 2px;
+ border-radius: 1px;
position: absolute;
inset: 0;
z-index: 1;
@@ -735,7 +723,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`]
appearance: none;
background: transparent;
border: none;
- border-radius: 2px;
+ border-radius: 1px;
color: #757575;
fill: currentColor;
cursor: pointer;
@@ -903,10 +891,6 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio
box-sizing: inherit;
}
-.emotion-2 {
- margin-bottom: calc(4px * 2);
-}
-
.components-panel__row .emotion-2 {
margin-bottom: inherit;
}
@@ -978,7 +962,7 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio
appearance: none;
background: transparent;
border: none;
- border-radius: 2px;
+ border-radius: 1px;
color: #757575;
fill: currentColor;
cursor: pointer;
diff --git a/packages/components/src/toggle-group-control/test/index.tsx b/packages/components/src/toggle-group-control/test/index.tsx
index 661bbb9fc37bab..170db01ae523c2 100644
--- a/packages/components/src/toggle-group-control/test/index.tsx
+++ b/packages/components/src/toggle-group-control/test/index.tsx
@@ -15,7 +15,7 @@ import { formatLowercase, formatUppercase } from '@wordpress/icons';
*/
import Button from '../../button';
import {
- ToggleGroupControl,
+ ToggleGroupControl as _ToggleGroupControl,
ToggleGroupControlOption,
ToggleGroupControlOptionIcon,
} from '../index';
@@ -27,6 +27,10 @@ const hoverOutside = async () => {
await hover( document.body, { clientX: 10, clientY: 10 } );
};
+const ToggleGroupControl = ( props: ToggleGroupControlProps ) => {
+ return <_ToggleGroupControl { ...props } __nextHasNoMarginBottom />;
+};
+
const ControlledToggleGroupControl = ( {
value: valueProp,
onChange,
diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts
index 999a25df8bdd40..86efc5224077f4 100644
--- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts
+++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts
@@ -37,7 +37,7 @@ export const buttonView = ( {
appearance: none;
background: transparent;
border: none;
- border-radius: ${ CONFIG.controlBorderRadius };
+ border-radius: ${ CONFIG.radiusXSmall };
color: ${ COLORS.gray[ 700 ] };
fill: currentColor;
cursor: pointer;
@@ -122,7 +122,7 @@ const isIconStyles = ( {
export const backdropView = css`
background: ${ COLORS.gray[ 900 ] };
- border-radius: ${ CONFIG.controlBorderRadius };
+ border-radius: ${ CONFIG.radiusXSmall };
position: absolute;
inset: 0;
z-index: 1;
diff --git a/packages/components/src/toggle-group-control/toggle-group-control/README.md b/packages/components/src/toggle-group-control/toggle-group-control/README.md
index 6db7ec20544185..ca5c5d14eb6b5a 100644
--- a/packages/components/src/toggle-group-control/toggle-group-control/README.md
+++ b/packages/components/src/toggle-group-control/toggle-group-control/README.md
@@ -88,6 +88,13 @@ The value of the `ToggleGroupControl`.
- Required: No
+### `__next40pxDefaultSize`: `boolean`
+
+Start opting into the larger default height that will become the default size in a future version.
+
+- Required: No
+- Default: `false`
+
### `__nextHasNoMarginBottom`: `boolean`
Start opting into the new margin-free styles that will become the default in a future version.
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 c363cb6577d839..6baadd65dc5ff6 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,6 +3,7 @@
*/
import type { ForwardedRef } from 'react';
import * as Ariakit from '@ariakit/react';
+import { useStoreState } from '@ariakit/react';
/**
* WordPress dependencies
@@ -66,7 +67,7 @@ function UnforwardedToggleGroupControlAsRadioGroup(
setValue: wrappedOnChangeProp,
} );
- const selectedValue = radio.useState( 'value' );
+ const selectedValue = useStoreState( radio, 'value' );
const setValue = radio.setValue;
const groupContextValue = useMemo(
diff --git a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx
index 8138b76505fe50..1c86c93548f6df 100644
--- a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx
+++ b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx
@@ -72,6 +72,7 @@ function UnconnectedToggleGroupControl(
{ ! hideLabelFromVision && (
diff --git a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts
index 7310024706e1de..8d01c150a45eaf 100644
--- a/packages/components/src/toggle-group-control/toggle-group-control/styles.ts
+++ b/packages/components/src/toggle-group-control/toggle-group-control/styles.ts
@@ -19,7 +19,7 @@ export const toggleGroupControl = ( {
} ) => css`
background: ${ COLORS.ui.background };
border: 1px solid transparent;
- border-radius: ${ CONFIG.controlBorderRadius };
+ border-radius: ${ CONFIG.radiusSmall };
display: inline-flex;
min-width: 0;
position: relative;
diff --git a/packages/components/src/toolbar/toolbar/style.scss b/packages/components/src/toolbar/toolbar/style.scss
index eccfc3bf705cbd..c0cabacb84c77e 100644
--- a/packages/components/src/toolbar/toolbar/style.scss
+++ b/packages/components/src/toolbar/toolbar/style.scss
@@ -1,7 +1,7 @@
.components-accessible-toolbar {
display: inline-flex;
border: $border-width solid $gray-900;
- border-radius: $radius-block-ui;
+ border-radius: $radius-small;
flex-shrink: 0;
& > .components-toolbar-group:last-child {
@@ -47,7 +47,7 @@
content: "";
position: absolute;
display: block;
- border-radius: $radius-block-ui;
+ border-radius: $radius-small;
height: $grid-unit-40;
// Position the focus rectangle.
diff --git a/packages/components/src/tools-panel/styles.ts b/packages/components/src/tools-panel/styles.ts
index 1da1003c0462e3..11536e98a128a9 100644
--- a/packages/components/src/tools-panel/styles.ts
+++ b/packages/components/src/tools-panel/styles.ts
@@ -21,7 +21,7 @@ const toolsPanelGrid = {
grid-template-columns: ${ `repeat( ${ columns }, minmax(0, 1fr) )` };
`,
spacing: css`
- column-gap: ${ space( 2 ) };
+ column-gap: ${ space( 4 ) };
row-gap: ${ space( 4 ) };
`,
item: {
diff --git a/packages/components/src/tooltip/index.tsx b/packages/components/src/tooltip/index.tsx
index e832119393282a..7ce9311fc942ea 100644
--- a/packages/components/src/tooltip/index.tsx
+++ b/packages/components/src/tooltip/index.tsx
@@ -2,6 +2,7 @@
* External dependencies
*/
import * as Ariakit from '@ariakit/react';
+import { useStoreState } from '@ariakit/react';
import clsx from 'clsx';
/**
@@ -93,7 +94,7 @@ function UnforwardedTooltip(
placement: computedPlacement,
showTimeout: delay,
} );
- const mounted = tooltipStore.useState( 'mounted' );
+ const mounted = useStoreState( tooltipStore, 'mounted' );
if ( isNestedInTooltip ) {
return isOnlyChild ? (
diff --git a/packages/components/src/tooltip/style.scss b/packages/components/src/tooltip/style.scss
index feda6cfa81c887..eaac8b3ad1c7f6 100644
--- a/packages/components/src/tooltip/style.scss
+++ b/packages/components/src/tooltip/style.scss
@@ -1,7 +1,7 @@
.components-tooltip {
background: $black;
font-family: $default-font;
- border-radius: $radius-block-ui;
+ border-radius: $radius-small;
color: $gray-100;
text-align: center;
line-height: 1.4;
diff --git a/packages/components/src/tree-select/index.tsx b/packages/components/src/tree-select/index.tsx
index 599dee4402ec72..bd92807bff4cc9 100644
--- a/packages/components/src/tree-select/index.tsx
+++ b/packages/components/src/tree-select/index.tsx
@@ -10,6 +10,15 @@ import { decodeEntities } from '@wordpress/html-entities';
import { SelectControl } from '../select-control';
import type { TreeSelectProps, Tree, Truthy } from './types';
import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props';
+import { ContextSystemProvider } from '../context';
+
+const CONTEXT_VALUE = {
+ BaseControl: {
+ // Temporary during deprecation grace period: Overrides the underlying `__associatedWPComponentName`
+ // via the context system to override the value set by SelectControl.
+ _overrides: { __associatedWPComponentName: 'TreeSelect' },
+ },
+};
function getSelectOptions(
tree: Tree[],
@@ -91,11 +100,13 @@ export function TreeSelect( props: TreeSelectProps ) {
}, [ noOptionLabel, tree ] );
return (
-
+
+
+
);
}
diff --git a/packages/components/src/tree-select/stories/index.story.tsx b/packages/components/src/tree-select/stories/index.story.tsx
index 0a4212dc791227..33103786bbc541 100644
--- a/packages/components/src/tree-select/stories/index.story.tsx
+++ b/packages/components/src/tree-select/stories/index.story.tsx
@@ -48,6 +48,7 @@ const TreeSelectWithState: StoryFn< typeof TreeSelect > = ( props ) => {
export const Default = TreeSelectWithState.bind( {} );
Default.args = {
+ __nextHasNoMarginBottom: true,
label: 'Label Text',
noOptionLabel: 'No parent page',
help: 'Help text to explain the select control.',
diff --git a/packages/components/src/unit-control/README.md b/packages/components/src/unit-control/README.md
index b3c0496f86b800..dd54de80357d8e 100644
--- a/packages/components/src/unit-control/README.md
+++ b/packages/components/src/unit-control/README.md
@@ -147,3 +147,10 @@ Example:
```
- Required: No
+
+### `__next40pxDefaultSize`: `boolean`
+
+Start opting into the larger default height that will become the default size in a future version.
+
+- Required: No
+- Default: `false`
diff --git a/packages/components/src/unit-control/styles/unit-control-styles.ts b/packages/components/src/unit-control/styles/unit-control-styles.ts
index 321bfb8406569d..5f59771bd48a6a 100644
--- a/packages/components/src/unit-control/styles/unit-control-styles.ts
+++ b/packages/components/src/unit-control/styles/unit-control-styles.ts
@@ -135,7 +135,7 @@ export const UnitSelect = styled.select< SelectProps >`
&&& {
appearance: none;
background: transparent;
- border-radius: 2px;
+ border-radius: ${ CONFIG.radiusXSmall };
border: none;
display: block;
outline: none;
diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js
index ba92813bdbfb0f..0ad1b3294a926b 100644
--- a/packages/components/src/utils/config-values.js
+++ b/packages/components/src/utils/config-values.js
@@ -14,7 +14,6 @@ const CONTROL_PROPS = {
controlPaddingXLarge: `calc(${ CONTROL_PADDING_X } * 1.3334)`,
controlPaddingXSmall: `calc(${ CONTROL_PADDING_X } / 1.3334)`,
controlBackgroundColor: COLORS.white,
- controlBorderRadius: '2px',
controlBoxShadow: 'transparent',
controlBoxShadowFocus: `0 0 0 0.5px ${ COLORS.theme.accent }`,
controlDestructiveBorderColor: COLORS.alert.red,
@@ -48,7 +47,6 @@ export default Object.assign( {}, CONTROL_PROPS, TOGGLE_GROUP_CONTROL_PROPS, {
radiusLarge: '8px',
radiusFull: '9999px',
radiusRound: '50%',
- radiusBlockUi: '2px',
borderWidth: '1px',
borderWidthFocus: '1.5px',
borderWidthTab: '4px',
@@ -73,7 +71,10 @@ export default Object.assign( {}, CONTROL_PROPS, TOGGLE_GROUP_CONTROL_PROPS, {
cardPaddingSmall: `${ space( 4 ) }`,
cardPaddingMedium: `${ space( 4 ) } ${ space( 6 ) }`,
cardPaddingLarge: `${ space( 6 ) } ${ space( 8 ) }`,
- popoverShadow: `0 0.7px 1px rgba(0, 0, 0, 0.1), 0 1.2px 1.7px -0.2px rgba(0, 0, 0, 0.1), 0 2.3px 3.3px -0.5px rgba(0, 0, 0, 0.1)`,
+ elevationXSmall: `0 0.7px 1px rgba(0, 0, 0, 0.1), 0 1.2px 1.7px -0.2px rgba(0, 0, 0, 0.1), 0 2.3px 3.3px -0.5px rgba(0, 0, 0, 0.1)`,
+ elevationSmall: `0 0.7px 1px 0 rgba(0, 0, 0, 0.12), 0 2.2px 3.7px -0.2px rgba(0, 0, 0, 0.12), 0 5.3px 7.3px -0.5px rgba(0, 0, 0, 0.12)`,
+ elevationMedium: `0 0.7px 1px 0 rgba(0, 0, 0, 0.14), 0 4.2px 5.7px -0.2px rgba(0, 0, 0, 0.14), 0 7.3px 9.3px -0.5px rgba(0, 0, 0, 0.14)`,
+ elevationLarge: `0 0.7px 1px rgba(0, 0, 0, 0.15), 0 2.7px 3.8px -0.2px rgba(0, 0, 0, 0.15), 0 5.5px 7.8px -0.3px rgba(0, 0, 0, 0.15), 0.1px 11.5px 16.4px -0.5px rgba(0, 0, 0, 0.15)`,
surfaceBackgroundColor: COLORS.white,
surfaceBackgroundSubtleColor: '#F3F3F3',
surfaceBackgroundTintColor: '#F5F5F5',
diff --git a/packages/components/src/utils/input/base.js b/packages/components/src/utils/input/base.js
deleted file mode 100644
index f03a1d9c77abbe..00000000000000
--- a/packages/components/src/utils/input/base.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * External dependencies
- */
-import { css } from '@emotion/react';
-
-/**
- * Internal dependencies
- */
-import { COLORS } from '../colors-values';
-import { CONFIG } from '../';
-
-export const inputStyleNeutral = css`
- box-shadow: 0 0 0 transparent;
- border-radius: ${ CONFIG.radiusBlockUi };
- border: ${ CONFIG.borderWidth } solid ${ COLORS.ui.border };
-
- @media not ( prefers-reduced-motion ) {
- transition: box-shadow 0.1s linear;
- }
-`;
-
-export const inputStyleFocus = css`
- border-color: ${ COLORS.theme.accent };
- box-shadow: 0 0 0
- calc( ${ CONFIG.borderWidthFocus } - ${ CONFIG.borderWidth } )
- ${ COLORS.theme.accent };
-
- // Windows High Contrast mode will show this outline, but not the box-shadow.
- outline: 2px solid transparent;
-`;
diff --git a/packages/components/src/utils/input/index.js b/packages/components/src/utils/input/index.js
deleted file mode 100644
index 23fd731db99d94..00000000000000
--- a/packages/components/src/utils/input/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './input-control';
diff --git a/packages/components/src/utils/input/input-control.js b/packages/components/src/utils/input/input-control.js
deleted file mode 100644
index 8af838247d9c25..00000000000000
--- a/packages/components/src/utils/input/input-control.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * External dependencies
- */
-import { css } from '@emotion/react';
-
-/**
- * Internal dependencies
- */
-import { inputStyleNeutral, inputStyleFocus } from './base';
-import { font } from '../font';
-import { COLORS } from '../colors-values';
-import { breakpoint } from '../breakpoint';
-
-export const inputControl = css`
- display: block;
- font-family: ${ font( 'default.fontFamily' ) };
- padding: 6px 8px;
- ${ inputStyleNeutral };
-
- /* Fonts smaller than 16px causes mobile safari to zoom. */
- font-size: ${ font( 'mobileTextMinFontSize' ) };
- /* Override core line-height. To be reviewed. */
- line-height: normal;
-
- ${ breakpoint( 'small' ) } {
- font-size: ${ font( 'default.fontSize' ) };
- /* Override core line-height. To be reviewed. */
- line-height: normal;
- }
-
- &:focus {
- ${ inputStyleFocus }
- }
-
- // Use opacity to work in various editor styles.
- &::-webkit-input-placeholder {
- color: ${ COLORS.ui.darkGrayPlaceholder };
- }
-
- &::-moz-placeholder {
- opacity: 1; // Necessary because Firefox reduces this from 1.
- color: ${ COLORS.ui.darkGrayPlaceholder };
- }
-
- &:-ms-input-placeholder {
- color: ${ COLORS.ui.darkGrayPlaceholder };
- }
-
- .is-dark-theme & {
- &::-webkit-input-placeholder {
- color: ${ COLORS.ui.lightGrayPlaceholder };
- }
-
- &::-moz-placeholder {
- opacity: 1; // Necessary because Firefox reduces this from 1.
- color: ${ COLORS.ui.lightGrayPlaceholder };
- }
-
- &:-ms-input-placeholder {
- color: ${ COLORS.ui.lightGrayPlaceholder };
- }
- }
-`;
diff --git a/packages/core-data/src/entity-types/wp-template.ts b/packages/core-data/src/entity-types/wp-template.ts
index ac6db09035f193..70d3e40c295dcf 100644
--- a/packages/core-data/src/entity-types/wp-template.ts
+++ b/packages/core-data/src/entity-types/wp-template.ts
@@ -73,6 +73,10 @@ declare module './base-entity-records' {
* Post ID.
*/
wp_id: number;
+ /**
+ * Plugin that registered the template.
+ */
+ plugin?: string;
/**
* Theme file exists.
*/
diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md
index 239a69651a1f27..5969d8b462d65b 100644
--- a/packages/dataviews/CHANGELOG.md
+++ b/packages/dataviews/CHANGELOG.md
@@ -2,6 +2,20 @@
## Unreleased
+## New features
+
+- Support using a component for field headers or names by providing a `header` property in the field object. The string `label` property (or `id`) is still mandatory. ([#64642](https://github.com/WordPress/gutenberg/pull/64642)).
+
+## Internal
+
+- The "move left/move right" controls in the table layout (popup displayed on cliking header) are always visible. ([#64646](https://github.com/WordPress/gutenberg/pull/64646)). Before this, its visibility depending on filters, enableSorting, and enableHiding.
+- Filters no longer display the elements' description. ([#64674](https://github.com/WordPress/gutenberg/pull/64674))
+
+
+## Enhancements
+
+- Adjust layout of filter / actions row, increase width of search control when the container is narrower. ([#64681](https://github.com/WordPress/gutenberg/pull/64681)).
+
## 4.1.0 (2024-08-07)
## Internal
@@ -16,6 +30,7 @@
- `setSelection` prop has been removed. Please use `onChangeSelection` instead.
- `header` field property has been renamed to `label`.
- `DataForm`'s `visibleFields` prop has been renamed to `fields`.
+- `DataForm`'s `onChange` prop has been update to receive as argument only the fields that have changed.
### New features
diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md
index c82a748df98858..6c1e2af26386c7 100644
--- a/packages/dataviews/README.md
+++ b/packages/dataviews/README.md
@@ -30,6 +30,8 @@ const Example = () => {
};
```
+
{ ! isScheduled && (
-
+
{ viewPostLabel }
) }
{ addNewPostLabel }
diff --git a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap
index 42fc0f7fe0f245..9331376d7f9c35 100644
--- a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap
+++ b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap
@@ -120,7 +120,7 @@ exports[`PostPublishPanel should render the post-publish panel if the post is pu
post address
Copy
@@ -143,11 +143,11 @@ exports[`PostPublishPanel should render the post-publish panel if the post is pu
class="post-publish-panel__postpublish-buttons"
>
@@ -312,7 +312,7 @@ exports[`PostPublishPanel should render the post-publish panel if the post is sc
post address
Copy
@@ -335,11 +335,11 @@ exports[`PostPublishPanel should render the post-publish panel if the post is sc
class="post-publish-panel__postpublish-buttons"
>
diff --git a/packages/editor/src/components/post-slug/index.js b/packages/editor/src/components/post-slug/index.js
index cc86405157b88f..1a4f8e93d7565c 100644
--- a/packages/editor/src/components/post-slug/index.js
+++ b/packages/editor/src/components/post-slug/index.js
@@ -24,6 +24,7 @@ function PostSlugControl() {
return (
.components-v-stack {
- gap: $grid-unit-15;
- }
-
- // TODO: it's not great to override component styles.. This might be resolved
- // by the new radio control component.
- .components-radio-control__option {
- align-items: flex-start;
- }
-
- label {
- .components-text {
- display: block;
- margin-top: $grid-unit-05;
- }
- }
- }
-
.editor-change-status__password-legend {
padding: 0;
margin-bottom: $grid-unit-10;
diff --git a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js
index 79039d0a5d0d25..112bfaabd0c8ec 100644
--- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js
+++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js
@@ -12,6 +12,7 @@ import {
withFilters,
Flex,
FlexItem,
+ SearchControl,
} from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { useDebounce } from '@wordpress/compose';
@@ -406,7 +407,8 @@ export function HierarchicalTermSelector( { slug } ) {
return (
{ showFilter && (
-
{ isEditable && (
- { __( 'Customize the last part of the URL. ' ) }
-
- { __( 'Learn more.' ) }
-
+ { createInterpolateElement(
+ __(
+ 'Customize the last part of the URL. Learn more.'
+ ),
+ {
+ a: (
+
+ ),
+ }
+ ) }
) }
diff --git a/packages/editor/src/components/save-publish-panels/style.scss b/packages/editor/src/components/save-publish-panels/style.scss
index 74dcf113f0479b..1de6420a0a6837 100644
--- a/packages/editor/src/components/save-publish-panels/style.scss
+++ b/packages/editor/src/components/save-publish-panels/style.scss
@@ -3,7 +3,7 @@
.editor-layout__toggle-sidebar-panel,
.editor-layout__toggle-entities-saved-states-panel {
z-index: z-index(".editor-layout__toggle-sidebar-panel");
- position: fixed !important; // Need to override the default relative positionning
+ position: fixed !important; // Necessary to override the default relative positioning.
top: -9999em;
bottom: auto;
left: auto;
@@ -11,8 +11,8 @@
box-sizing: border-box;
width: $sidebar-width;
background-color: $white;
- border: 1px dotted $gray-300;
- height: auto !important; // Need to override the default sidebar positionnings
+ border: $border-width dotted $gray-300;
+ height: auto !important; // Necessary to override the default sidebar positioning.
padding: $grid-unit-30;
display: flex;
justify-content: center;
diff --git a/packages/editor/src/components/sidebar/post-summary.js b/packages/editor/src/components/sidebar/post-summary.js
index a6a95d36388ba5..b19848f2247063 100644
--- a/packages/editor/src/components/sidebar/post-summary.js
+++ b/packages/editor/src/components/sidebar/post-summary.js
@@ -8,7 +8,6 @@ import { useSelect } from '@wordpress/data';
* Internal dependencies
*/
import PluginPostStatusInfo from '../plugin-post-status-info';
-import PostActions from '../post-actions';
import PostAuthorPanel from '../post-author/panel';
import PostCardPanel from '../post-card-panel';
import PostContentInformation from '../post-content-information';
@@ -36,16 +35,23 @@ import { PrivatePostLastRevision } from '../post-last-revision';
const PANEL_NAME = 'post-status';
export default function PostSummary( { onActionPerformed } ) {
- const { isRemovedPostStatusPanel } = useSelect( ( select ) => {
- // We use isEditorPanelRemoved to hide the panel if it was programatically removed. We do
- // not use isEditorPanelEnabled since this panel should not be disabled through the UI.
- const { isEditorPanelRemoved, getCurrentPostType } =
- select( editorStore );
- return {
- isRemovedPostStatusPanel: isEditorPanelRemoved( PANEL_NAME ),
- postType: getCurrentPostType(),
- };
- }, [] );
+ const { isRemovedPostStatusPanel, postType, postId } = useSelect(
+ ( select ) => {
+ // We use isEditorPanelRemoved to hide the panel if it was programatically removed. We do
+ // not use isEditorPanelEnabled since this panel should not be disabled through the UI.
+ const {
+ isEditorPanelRemoved,
+ getCurrentPostType,
+ getCurrentPostId,
+ } = select( editorStore );
+ return {
+ isRemovedPostStatusPanel: isEditorPanelRemoved( PANEL_NAME ),
+ postType: getCurrentPostType(),
+ postId: getCurrentPostId(),
+ };
+ },
+ []
+ );
return (
@@ -54,11 +60,9 @@ export default function PostSummary( { onActionPerformed } ) {
<>
- }
+ postType={ postType }
+ postId={ postId }
+ onActionPerformed={ onActionPerformed }
/>
diff --git a/packages/editor/src/components/site-discussion/style.scss b/packages/editor/src/components/site-discussion/style.scss
index 2c54424207ea5c..dc0608f0cf3ab1 100644
--- a/packages/editor/src/components/site-discussion/style.scss
+++ b/packages/editor/src/components/site-discussion/style.scss
@@ -3,17 +3,3 @@
padding: $grid-unit-20;
}
-.editor-site-discussion__options {
- // TODO: it's not great to override component styles.. This might be resolved
- // by the new radio control component.
- .components-radio-control__option {
- align-items: flex-start;
- }
-
- label {
- .components-text {
- display: block;
- margin-top: $grid-unit-05;
- }
- }
-}
diff --git a/packages/editor/src/components/template-content-panel/index.js b/packages/editor/src/components/template-content-panel/index.js
index af9721dfdf2b32..41e170e76daef5 100644
--- a/packages/editor/src/components/template-content-panel/index.js
+++ b/packages/editor/src/components/template-content-panel/index.js
@@ -59,7 +59,10 @@ export default function TemplateContentPanel() {
const { enableComplementaryArea } = useDispatch( interfaceStore );
- if ( renderingMode === 'post-only' && postType !== TEMPLATE_POST_TYPE ) {
+ if (
+ ( renderingMode === 'post-only' && postType !== TEMPLATE_POST_TYPE ) ||
+ clientIds.length === 0
+ ) {
return null;
}
diff --git a/packages/editor/src/dataviews/actions/duplicate-pattern.tsx b/packages/editor/src/dataviews/actions/duplicate-pattern.tsx
new file mode 100644
index 00000000000000..98f43a27c3628c
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/duplicate-pattern.tsx
@@ -0,0 +1,40 @@
+/**
+ * WordPress dependencies
+ */
+import { _x } from '@wordpress/i18n';
+// @ts-ignore
+import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
+import type { Action } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import { unlock } from '../../lock-unlock';
+import type { Pattern } from '../types';
+
+// Patterns.
+const { CreatePatternModalContents, useDuplicatePatternProps } =
+ unlock( patternsPrivateApis );
+
+const duplicatePattern: Action< Pattern > = {
+ id: 'duplicate-pattern',
+ label: _x( 'Duplicate', 'action label' ),
+ isEligible: ( item ) => item.type !== 'wp_template_part',
+ modalHeader: _x( 'Duplicate pattern', 'action label' ),
+ RenderModal: ( { items, closeModal } ) => {
+ const [ item ] = items;
+ const duplicatedProps = useDuplicatePatternProps( {
+ pattern: item,
+ onSuccess: () => closeModal?.(),
+ } );
+ return (
+
+ );
+ },
+};
+
+export default duplicatePattern;
diff --git a/packages/editor/src/dataviews/actions/duplicate-post.native.tsx b/packages/editor/src/dataviews/actions/duplicate-post.native.tsx
new file mode 100644
index 00000000000000..5468aa649abbd4
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/duplicate-post.native.tsx
@@ -0,0 +1,3 @@
+const duplicatePost = undefined;
+
+export default duplicatePost;
diff --git a/packages/editor/src/dataviews/actions/duplicate-post.tsx b/packages/editor/src/dataviews/actions/duplicate-post.tsx
new file mode 100644
index 00000000000000..0979d30da39519
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/duplicate-post.tsx
@@ -0,0 +1,174 @@
+/**
+ * WordPress dependencies
+ */
+import { useDispatch } from '@wordpress/data';
+import { decodeEntities } from '@wordpress/html-entities';
+import { store as coreStore } from '@wordpress/core-data';
+import { __, sprintf, _x } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+import { useState } from '@wordpress/element';
+import { DataForm } from '@wordpress/dataviews';
+import {
+ Button,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
+import type { Action } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import { getItemTitle } from '../../dataviews/actions/utils';
+import type { CoreDataError, BasePost } from '../types';
+import { titleField } from '../fields';
+
+const fields = [ titleField ];
+const formDuplicateAction = {
+ fields: [ 'title' ],
+};
+
+const duplicatePost: Action< BasePost > = {
+ id: 'duplicate-post',
+ label: _x( 'Duplicate', 'action label' ),
+ isEligible( { status } ) {
+ return status !== 'trash';
+ },
+ RenderModal: ( { items, closeModal, onActionPerformed } ) => {
+ const [ item, setItem ] = useState< BasePost >( {
+ ...items[ 0 ],
+ title: sprintf(
+ /* translators: %s: Existing template title */
+ __( '%s (Copy)' ),
+ getItemTitle( items[ 0 ] )
+ ),
+ } );
+
+ const [ isCreatingPage, setIsCreatingPage ] = useState( false );
+ const { saveEntityRecord } = useDispatch( coreStore );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+
+ async function createPage( event: React.FormEvent ) {
+ event.preventDefault();
+
+ if ( isCreatingPage ) {
+ return;
+ }
+
+ const newItemOject = {
+ status: 'draft',
+ title: item.title,
+ slug: item.title || __( 'No title' ),
+ comment_status: item.comment_status,
+ content:
+ typeof item.content === 'string'
+ ? item.content
+ : item.content.raw,
+ excerpt:
+ typeof item.excerpt === 'string'
+ ? item.excerpt
+ : item.excerpt?.raw,
+ meta: item.meta,
+ parent: item.parent,
+ password: item.password,
+ template: item.template,
+ format: item.format,
+ featured_media: item.featured_media,
+ menu_order: item.menu_order,
+ ping_status: item.ping_status,
+ };
+ const assignablePropertiesPrefix = 'wp:action-assign-';
+ // Get all the properties that the current user is able to assign normally author, categories, tags,
+ // and custom taxonomies.
+ const assignableProperties = Object.keys( item?._links || {} )
+ .filter( ( property ) =>
+ property.startsWith( assignablePropertiesPrefix )
+ )
+ .map( ( property ) =>
+ property.slice( assignablePropertiesPrefix.length )
+ );
+ assignableProperties.forEach( ( property ) => {
+ if ( item.hasOwnProperty( property ) ) {
+ // @ts-ignore
+ newItemOject[ property ] = item[ property ];
+ }
+ } );
+ setIsCreatingPage( true );
+ try {
+ const newItem = await saveEntityRecord(
+ 'postType',
+ item.type,
+ newItemOject,
+ { throwOnError: true }
+ );
+
+ createSuccessNotice(
+ sprintf(
+ // translators: %s: Title of the created template e.g: "Category".
+ __( '"%s" successfully created.' ),
+ decodeEntities( newItem.title?.rendered || item.title )
+ ),
+ {
+ id: 'duplicate-post-action',
+ type: 'snackbar',
+ }
+ );
+
+ if ( onActionPerformed ) {
+ onActionPerformed( [ newItem ] );
+ }
+ } catch ( error ) {
+ const typedError = error as CoreDataError;
+ const errorMessage =
+ typedError.message && typedError.code !== 'unknown_error'
+ ? typedError.message
+ : __( 'An error occurred while duplicating the page.' );
+
+ createErrorNotice( errorMessage, {
+ type: 'snackbar',
+ } );
+ } finally {
+ setIsCreatingPage( false );
+ closeModal?.();
+ }
+ }
+
+ return (
+
+ );
+ },
+};
+
+export default duplicatePost;
diff --git a/packages/editor/src/dataviews/actions/duplicate-template-part.tsx b/packages/editor/src/dataviews/actions/duplicate-template-part.tsx
new file mode 100644
index 00000000000000..fa3cf39ba76268
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/duplicate-template-part.tsx
@@ -0,0 +1,70 @@
+/**
+ * WordPress dependencies
+ */
+import { useDispatch } from '@wordpress/data';
+import { __, sprintf, _x } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+import { useMemo } from '@wordpress/element';
+// @ts-ignore
+import { parse } from '@wordpress/blocks';
+import type { Action } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import { TEMPLATE_PART_POST_TYPE } from '../../store/constants';
+import { CreateTemplatePartModalContents } from '../../components/create-template-part-modal';
+import { getItemTitle } from './utils';
+import type { TemplatePart } from '../types';
+
+const duplicateTemplatePart: Action< TemplatePart > = {
+ id: 'duplicate-template-part',
+ label: _x( 'Duplicate', 'action label' ),
+ isEligible: ( item ) => item.type === TEMPLATE_PART_POST_TYPE,
+ modalHeader: _x( 'Duplicate template part', 'action label' ),
+ RenderModal: ( { items, closeModal } ) => {
+ const [ item ] = items;
+ const blocks = useMemo( () => {
+ return (
+ item.blocks ??
+ parse(
+ typeof item.content === 'string'
+ ? item.content
+ : item.content.raw,
+ {
+ __unstableSkipMigrationLogs: true,
+ }
+ )
+ );
+ }, [ item.content, item.blocks ] );
+ const { createSuccessNotice } = useDispatch( noticesStore );
+ function onTemplatePartSuccess() {
+ createSuccessNotice(
+ sprintf(
+ // translators: %s: The new template part's title e.g. 'Call to action (copy)'.
+ __( '"%s" duplicated.' ),
+ getItemTitle( item )
+ ),
+ { type: 'snackbar', id: 'edit-site-patterns-success' }
+ );
+ closeModal?.();
+ }
+ return (
+
+ );
+ },
+};
+
+export default duplicateTemplatePart;
diff --git a/packages/editor/src/dataviews/actions/rename-post.tsx b/packages/editor/src/dataviews/actions/rename-post.tsx
new file mode 100644
index 00000000000000..ef9da271111ea2
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/rename-post.tsx
@@ -0,0 +1,146 @@
+/**
+ * WordPress dependencies
+ */
+import { useDispatch } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { __ } from '@wordpress/i18n';
+import { useState } from '@wordpress/element';
+// @ts-ignore
+import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
+import {
+ Button,
+ TextControl,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
+import type { Action } from '@wordpress/dataviews';
+import { store as noticesStore } from '@wordpress/notices';
+
+/**
+ * Internal dependencies
+ */
+import {
+ TEMPLATE_ORIGINS,
+ TEMPLATE_PART_POST_TYPE,
+ TEMPLATE_POST_TYPE,
+} from '../../store/constants';
+import { unlock } from '../../lock-unlock';
+import {
+ getItemTitle,
+ isTemplateRemovable,
+ isTemplate,
+ isTemplatePart,
+} from './utils';
+import type { CoreDataError, PostWithPermissions } from '../types';
+
+// Patterns.
+const { PATTERN_TYPES } = unlock( patternsPrivateApis );
+
+const renamePost: Action< PostWithPermissions > = {
+ id: 'rename-post',
+ label: __( 'Rename' ),
+ isEligible( post ) {
+ if ( post.status === 'trash' ) {
+ return false;
+ }
+ // Templates, template parts and patterns have special checks for renaming.
+ if (
+ ! [
+ TEMPLATE_POST_TYPE,
+ TEMPLATE_PART_POST_TYPE,
+ ...Object.values( PATTERN_TYPES ),
+ ].includes( post.type )
+ ) {
+ return post.permissions?.update;
+ }
+
+ // In the case of templates, we can only rename custom templates.
+ if ( isTemplate( post ) ) {
+ return (
+ isTemplateRemovable( post ) &&
+ post.is_custom &&
+ post.permissions?.update
+ );
+ }
+
+ if ( isTemplatePart( post ) ) {
+ return (
+ post.source === TEMPLATE_ORIGINS.custom &&
+ ! post?.has_theme_file &&
+ post.permissions?.update
+ );
+ }
+
+ return post.type === PATTERN_TYPES.user && post.permissions?.update;
+ },
+ RenderModal: ( { items, closeModal, onActionPerformed } ) => {
+ const [ item ] = items;
+ const [ title, setTitle ] = useState( () => getItemTitle( item ) );
+ const { editEntityRecord, saveEditedEntityRecord } =
+ useDispatch( coreStore );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+
+ async function onRename( event: React.FormEvent ) {
+ event.preventDefault();
+ try {
+ await editEntityRecord( 'postType', item.type, item.id, {
+ title,
+ } );
+ // Update state before saving rerenders the list.
+ setTitle( '' );
+ closeModal?.();
+ // Persist edited entity.
+ await saveEditedEntityRecord( 'postType', item.type, item.id, {
+ throwOnError: true,
+ } );
+ createSuccessNotice( __( 'Name updated' ), {
+ type: 'snackbar',
+ } );
+ onActionPerformed?.( items );
+ } catch ( error ) {
+ const typedError = error as CoreDataError;
+ const errorMessage =
+ typedError.message && typedError.code !== 'unknown_error'
+ ? typedError.message
+ : __( 'An error occurred while updating the name' );
+ createErrorNotice( errorMessage, { type: 'snackbar' } );
+ }
+ }
+
+ return (
+
+ );
+ },
+};
+
+export default renamePost;
diff --git a/packages/editor/src/dataviews/actions/reorder-page.native.tsx b/packages/editor/src/dataviews/actions/reorder-page.native.tsx
new file mode 100644
index 00000000000000..61e4733b6c6bd9
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/reorder-page.native.tsx
@@ -0,0 +1,3 @@
+const reorderPage = undefined;
+
+export default reorderPage;
diff --git a/packages/editor/src/dataviews/actions/reorder-page.tsx b/packages/editor/src/dataviews/actions/reorder-page.tsx
new file mode 100644
index 00000000000000..1820884d8d8c73
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/reorder-page.tsx
@@ -0,0 +1,125 @@
+/**
+ * WordPress dependencies
+ */
+import { useDispatch } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { __ } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+import { useState } from '@wordpress/element';
+import { DataForm, isItemValid } from '@wordpress/dataviews';
+import {
+ Button,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
+import type { Action, RenderModalProps } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { CoreDataError, BasePost } from '../types';
+import { orderField } from '../fields';
+
+const fields = [ orderField ];
+const formOrderAction = {
+ fields: [ 'menu_order' ],
+};
+
+function ReorderModal( {
+ items,
+ closeModal,
+ onActionPerformed,
+}: RenderModalProps< BasePost > ) {
+ const [ item, setItem ] = useState( items[ 0 ] );
+ const orderInput = item.menu_order;
+ const { editEntityRecord, saveEditedEntityRecord } =
+ useDispatch( coreStore );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+
+ async function onOrder( event: React.FormEvent ) {
+ event.preventDefault();
+
+ if ( ! isItemValid( item, fields, formOrderAction ) ) {
+ return;
+ }
+
+ try {
+ await editEntityRecord( 'postType', item.type, item.id, {
+ menu_order: orderInput,
+ } );
+ closeModal?.();
+ // Persist edited entity.
+ await saveEditedEntityRecord( 'postType', item.type, item.id, {
+ throwOnError: true,
+ } );
+ createSuccessNotice( __( 'Order updated.' ), {
+ type: 'snackbar',
+ } );
+ onActionPerformed?.( items );
+ } catch ( error ) {
+ const typedError = error as CoreDataError;
+ const errorMessage =
+ typedError.message && typedError.code !== 'unknown_error'
+ ? typedError.message
+ : __( 'An error occurred while updating the order' );
+ createErrorNotice( errorMessage, {
+ type: 'snackbar',
+ } );
+ }
+ }
+ const isSaveDisabled = ! isItemValid( item, fields, formOrderAction );
+ return (
+
+ );
+}
+
+const reorderPage: Action< BasePost > = {
+ id: 'order-pages',
+ label: __( 'Order' ),
+ isEligible( { status } ) {
+ return status !== 'trash';
+ },
+ RenderModal: ReorderModal,
+};
+
+export default reorderPage;
diff --git a/packages/editor/src/dataviews/actions/reset-post.tsx b/packages/editor/src/dataviews/actions/reset-post.tsx
index 59199555ddd4db..cc4cea8f5c82c0 100644
--- a/packages/editor/src/dataviews/actions/reset-post.tsx
+++ b/packages/editor/src/dataviews/actions/reset-post.tsx
@@ -32,7 +32,8 @@ const resetPost: Action< Post > = {
return (
isTemplateOrTemplatePart( item ) &&
item?.source === TEMPLATE_ORIGINS.custom &&
- item?.has_theme_file
+ ( Boolean( item.type === 'wp_template' && item?.plugin ) ||
+ item?.has_theme_file )
);
},
icon: backup,
diff --git a/packages/editor/src/dataviews/actions/utils.ts b/packages/editor/src/dataviews/actions/utils.ts
index 56c8c9f54c8507..33a2be16397f3f 100644
--- a/packages/editor/src/dataviews/actions/utils.ts
+++ b/packages/editor/src/dataviews/actions/utils.ts
@@ -12,11 +12,19 @@ import {
TEMPLATE_POST_TYPE,
} from '../../store/constants';
-import type { Post, TemplateOrTemplatePart } from '../types';
+import type { Post, TemplatePart, Template } from '../types';
+
+export function isTemplate( post: Post ): post is Template {
+ return post.type === TEMPLATE_POST_TYPE;
+}
+
+export function isTemplatePart( post: Post ): post is TemplatePart {
+ return post.type === TEMPLATE_PART_POST_TYPE;
+}
export function isTemplateOrTemplatePart(
p: Post
-): p is TemplateOrTemplatePart {
+): p is Template | TemplatePart {
return p.type === TEMPLATE_POST_TYPE || p.type === TEMPLATE_PART_POST_TYPE;
}
@@ -39,7 +47,7 @@ export function getItemTitle( item: Post ) {
* @param template The template entity to check.
* @return Whether the template is removable.
*/
-export function isTemplateRemovable( template: TemplateOrTemplatePart ) {
+export function isTemplateRemovable( template: Template | TemplatePart ) {
if ( ! template ) {
return false;
}
@@ -49,6 +57,8 @@ export function isTemplateRemovable( template: TemplateOrTemplatePart ) {
return (
[ template.source, template.source ].includes(
TEMPLATE_ORIGINS.custom
- ) && ! template.has_theme_file
+ ) &&
+ ! Boolean( template.type === 'wp_template' && template?.plugin ) &&
+ ! template.has_theme_file
);
}
diff --git a/packages/editor/src/dataviews/actions/view-post-revisions.tsx b/packages/editor/src/dataviews/actions/view-post-revisions.tsx
new file mode 100644
index 00000000000000..875b925b94f070
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/view-post-revisions.tsx
@@ -0,0 +1,47 @@
+/**
+ * WordPress dependencies
+ */
+import { addQueryArgs } from '@wordpress/url';
+import { __, sprintf } from '@wordpress/i18n';
+import type { Action } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { Post } from '../types';
+
+const viewPostRevisions: Action< Post > = {
+ id: 'view-post-revisions',
+ context: 'list',
+ label( items ) {
+ const revisionsCount =
+ items[ 0 ]._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0;
+ return sprintf(
+ /* translators: %s: number of revisions */
+ __( 'View revisions (%s)' ),
+ revisionsCount
+ );
+ },
+ isEligible( post ) {
+ if ( post.status === 'trash' ) {
+ return false;
+ }
+ const lastRevisionId =
+ post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id ?? null;
+ const revisionsCount =
+ post?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0;
+ return !! lastRevisionId && revisionsCount > 1;
+ },
+ callback( posts, { onActionPerformed } ) {
+ const post = posts[ 0 ];
+ const href = addQueryArgs( 'revision.php', {
+ revision: post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id,
+ } );
+ document.location.href = href;
+ if ( onActionPerformed ) {
+ onActionPerformed( posts );
+ }
+ },
+};
+
+export default viewPostRevisions;
diff --git a/packages/editor/src/dataviews/actions/view-post.tsx b/packages/editor/src/dataviews/actions/view-post.tsx
new file mode 100644
index 00000000000000..47eb1a66d019ad
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/view-post.tsx
@@ -0,0 +1,30 @@
+/**
+ * WordPress dependencies
+ */
+import { external } from '@wordpress/icons';
+import { __ } from '@wordpress/i18n';
+import type { Action } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { BasePost } from '../types';
+
+const viewPost: Action< BasePost > = {
+ id: 'view-post',
+ label: __( 'View' ),
+ isPrimary: true,
+ icon: external,
+ isEligible( post ) {
+ return post.status !== 'trash';
+ },
+ callback( posts, { onActionPerformed } ) {
+ const post = posts[ 0 ];
+ window.open( post?.link, '_blank' );
+ if ( onActionPerformed ) {
+ onActionPerformed( posts );
+ }
+ },
+};
+
+export default viewPost;
diff --git a/packages/editor/src/dataviews/fields/index.ts b/packages/editor/src/dataviews/fields/index.ts
new file mode 100644
index 00000000000000..b215172eaf7f02
--- /dev/null
+++ b/packages/editor/src/dataviews/fields/index.ts
@@ -0,0 +1,26 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import type { Field } from '@wordpress/dataviews';
+
+/**
+ * Internal dependencies
+ */
+import type { BasePost } from '../types';
+import { getItemTitle } from '../actions/utils';
+
+export const titleField: Field< BasePost > = {
+ type: 'text',
+ id: 'title',
+ label: __( 'Title' ),
+ placeholder: __( 'No title' ),
+ getValue: ( { item } ) => getItemTitle( item ),
+};
+
+export const orderField: Field< BasePost > = {
+ type: 'integer',
+ id: 'menu_order',
+ label: __( 'Order' ),
+ description: __( 'Determines the order of pages.' ),
+};
diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts
index 745cc0ad82e934..a9101e57dd08b5 100644
--- a/packages/editor/src/dataviews/store/private-actions.ts
+++ b/packages/editor/src/dataviews/store/private-actions.ts
@@ -9,14 +9,21 @@ import { doAction } from '@wordpress/hooks';
* Internal dependencies
*/
import deletePost from '../actions/delete-post';
+import duplicatePattern from '../actions/duplicate-pattern';
+import duplicateTemplatePart from '../actions/duplicate-template-part';
import exportPattern from '../actions/export-pattern';
import resetPost from '../actions/reset-post';
import trashPost from '../actions/trash-post';
import permanentlyDeletePost from '../actions/permanently-delete-post';
+import renamePost from '../actions/rename-post';
+import reorderPage from '../actions/reorder-page';
import restorePost from '../actions/restore-post';
import type { PostType } from '../types';
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';
+import duplicatePost from '../actions/duplicate-post';
+import viewPostRevisions from '../actions/view-post-revisions';
+import viewPost from '../actions/view-post';
export function registerEntityAction< Item >(
kind: string,
@@ -72,7 +79,41 @@ export const registerPostTypeActions =
.resolveSelect( coreStore )
.getPostType( postType ) ) as PostType;
+ const canCreate = await registry
+ .resolveSelect( coreStore )
+ .canUser( 'create', {
+ kind: 'postType',
+ name: postType,
+ } );
+ const currentTheme = await registry
+ .resolveSelect( coreStore )
+ .getCurrentTheme();
+
const actions = [
+ postTypeConfig.viewable ? viewPost : undefined,
+ !! postTypeConfig?.supports?.revisions
+ ? viewPostRevisions
+ : undefined,
+ // @ts-ignore
+ globalThis.IS_GUTENBERG_PLUGIN
+ ? ! [ 'wp_template', 'wp_block', 'wp_template_part' ].includes(
+ postTypeConfig.slug
+ ) &&
+ canCreate &&
+ duplicatePost
+ : undefined,
+ postTypeConfig.slug === 'wp_template_part' &&
+ canCreate &&
+ currentTheme?.is_block_theme
+ ? duplicateTemplatePart
+ : undefined,
+ canCreate && postTypeConfig.slug === 'wp_block'
+ ? duplicatePattern
+ : undefined,
+ postTypeConfig.supports?.title ? renamePost : undefined,
+ postTypeConfig?.supports?.[ 'page-attributes' ]
+ ? reorderPage
+ : undefined,
postTypeConfig.slug === 'wp_block' ? exportPattern : undefined,
resetPost,
restorePost,
@@ -83,7 +124,7 @@ export const registerPostTypeActions =
registry.batch( () => {
actions.forEach( ( action ) => {
- if ( action === undefined ) {
+ if ( ! action ) {
return;
}
unlock( registry.dispatch( editorStore ) ).registerEntityAction(
diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts
index 47f11c88bfb978..664c2dd417201c 100644
--- a/packages/editor/src/dataviews/types.ts
+++ b/packages/editor/src/dataviews/types.ts
@@ -7,27 +7,62 @@ type PostStatus =
| 'auto-draft'
| 'trash';
-export interface BasePost {
+export interface CommonPost {
status?: PostStatus;
title: string | { rendered: string } | { raw: string };
+ content: string | { raw: string; rendered: string };
type: string;
id: string | number;
+ blocks?: Object[];
+ _links?: Links;
}
-export interface TemplateOrTemplatePart extends BasePost {
- type: 'wp_template' | 'wp_template_part';
+
+interface Links {
+ 'predecessor-version'?: { href: string; id: number }[];
+ 'version-history'?: { href: string; count: number }[];
+ [ key: string ]: { href: string }[] | undefined;
+}
+
+export interface BasePost extends CommonPost {
+ comment_status?: 'open' | 'closed';
+ excerpt?: string | { raw: string; rendered: string };
+ meta?: Record< string, any >;
+ parent?: number;
+ password?: string;
+ template?: string;
+ format?: string;
+ featured_media?: number;
+ menu_order?: number;
+ ping_status?: 'open' | 'closed';
+ link?: string;
+}
+
+export interface Template extends CommonPost {
+ type: 'wp_template';
+ is_custom: boolean;
source: string;
+ origin: string;
+ plugin?: string;
has_theme_file: boolean;
id: string;
}
-export interface Pattern extends BasePost {
+export interface TemplatePart extends CommonPost {
+ type: 'wp_template_part';
+ source: string;
+ origin: string;
+ has_theme_file: boolean;
+ id: string;
+ area: string;
+}
+
+export interface Pattern extends CommonPost {
slug: string;
title: { raw: string };
- content: { raw: string } | string;
wp_pattern_sync_status: string;
}
-export type Post = TemplateOrTemplatePart | Pattern | BasePost;
+export type Post = Template | TemplatePart | Pattern | BasePost;
export type PostWithPermissions = Post & {
permissions: {
@@ -38,6 +73,12 @@ export type PostWithPermissions = Post & {
export interface PostType {
slug: string;
+ viewable: boolean;
+ supports?: {
+ 'page-attributes'?: boolean;
+ title?: boolean;
+ revisions?: boolean;
+ };
}
// Will be unnecessary after typescript 5.0 upgrade.
diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js
index a8a74d20261ce4..629600888a98eb 100644
--- a/packages/editor/src/private-apis.js
+++ b/packages/editor/src/private-apis.js
@@ -14,6 +14,7 @@ import BackButton from './components/header/back-button';
import CreateTemplatePartModal from './components/create-template-part-modal';
import Editor from './components/editor';
import PluginPostExcerpt from './components/post-excerpt/plugin';
+import PostCardPanel from './components/post-card-panel';
import PreferencesModal from './components/preferences-modal';
import { usePostActions } from './components/post-actions/actions';
import ToolsMoreMenuGroup from './components/more-menu/tools-more-menu-group';
@@ -40,6 +41,7 @@ lock( privateApis, {
GlobalStylesProvider,
mergeBaseAndUserConfigs,
PluginPostExcerpt,
+ PostCardPanel,
PreferencesModal,
usePostActions,
ToolsMoreMenuGroup,
diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js
index 0996d6eb8b9d32..e22929011256d5 100644
--- a/packages/editor/src/store/private-actions.js
+++ b/packages/editor/src/store/private-actions.js
@@ -269,7 +269,7 @@ export const revertTemplate =
const fileTemplatePath = addQueryArgs(
`${ templateEntityConfig.baseURL }/${ template.id }`,
- { context: 'edit', source: 'theme' }
+ { context: 'edit', source: template.origin }
);
const fileTemplate = await apiFetch( { path: fileTemplatePath } );
diff --git a/packages/editor/src/store/utils/is-template-revertable.js b/packages/editor/src/store/utils/is-template-revertable.js
index a09715af875bc2..2cb674920e3e4c 100644
--- a/packages/editor/src/store/utils/is-template-revertable.js
+++ b/packages/editor/src/store/utils/is-template-revertable.js
@@ -18,6 +18,7 @@ export default function isTemplateRevertable( templateOrTemplatePart ) {
return (
templateOrTemplatePart.source === TEMPLATE_ORIGINS.custom &&
- templateOrTemplatePart.has_theme_file
+ ( Boolean( templateOrTemplatePart?.plugin ) ||
+ templateOrTemplatePart?.has_theme_file )
);
}
diff --git a/packages/env/lib/wordpress.js b/packages/env/lib/wordpress.js
index 423547fad688b5..bd3c4a23f8ff5d 100644
--- a/packages/env/lib/wordpress.js
+++ b/packages/env/lib/wordpress.js
@@ -27,6 +27,30 @@ const { getCache, setCache } = require( './cache' );
* @typedef {'development'|'tests'|'all'} WPEnvironmentSelection
*/
+/**
+ * Utility function to check if a WordPress version is lower than another version.
+ *
+ * This is a non-comprehensive check only intended for this usage, to avoid pulling in a full semver library.
+ * It only considers the major and minor portions of the version and ignores the rest. Additionally, it assumes that
+ * the minor version is always a single digit (i.e. 0-9).
+ *
+ * Do not use this function for general version comparison, as it will not work for all cases.
+ *
+ * @param {string} version The version to check.
+ * @param {string} compareVersion The compare version to check whether the version is lower than.
+ * @return {boolean} True if the version is lower than the compare version, false otherwise.
+ */
+function isWPMajorMinorVersionLower( version, compareVersion ) {
+ const versionNumber = Number.parseFloat(
+ version.match( /^[0-9]+(\.[0-9]+)?/ )[ 0 ]
+ );
+ const compareVersionNumber = Number.parseFloat(
+ compareVersion.match( /^[0-9]+(\.[0-9]+)?/ )[ 0 ]
+ );
+
+ return versionNumber < compareVersionNumber;
+}
+
/**
* Checks a WordPress database connection. An error is thrown if the test is
* unsuccessful.
@@ -51,11 +75,28 @@ async function checkDatabaseConnection( { dockerComposeConfigPath, debug } ) {
* @param {Object} spinner A CLI spinner which indicates progress.
*/
async function configureWordPress( environment, config, spinner ) {
+ let wpVersion = '';
+ try {
+ wpVersion = await readWordPressVersion(
+ config.env[ environment ].coreSource,
+ spinner,
+ config.debug
+ );
+ } catch ( err ) {
+ // 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`;
// -eo pipefail exits the command as soon as anything fails in bash.
const setupCommands = [ 'set -eo pipefail', installCommand ];
+ // WordPress versions below 5.1 didn't use proper spacing in wp-config.
+ const configAnchor =
+ wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' )
+ ? `"define('WP_DEBUG',"`
+ : `"define( 'WP_DEBUG',"`;
+
// Set wp-config.php values.
for ( let [ key, value ] of Object.entries(
config.env[ environment ].config
@@ -68,7 +109,7 @@ async function configureWordPress( environment, config, spinner ) {
// Add quotes around string values to work with multi-word strings better.
value = typeof value === 'string' ? `"${ value }"` : value;
setupCommands.push(
- `wp config set ${ key } ${ value } --anchor="define( 'WP_DEBUG',"${
+ `wp config set ${ key } ${ value } --anchor=${ configAnchor }${
typeof value !== 'string' ? ' --raw' : ''
}`
);
@@ -98,6 +139,15 @@ async function configureWordPress( environment, config, spinner ) {
}
);
+ // WordPress versions below 5.1 didn't use proper spacing in wp-config.
+ // Additionally, WordPress versions below 5.4 used `dirname( __FILE__ )` instead of `__DIR__`.
+ let abspathDef = `define( 'ABSPATH', __DIR__ . '\\/' );`;
+ if ( wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.1' ) ) {
+ abspathDef = `define('ABSPATH', dirname(__FILE__) . '\\/');`;
+ } else if ( wpVersion && isWPMajorMinorVersionLower( wpVersion, '5.4' ) ) {
+ abspathDef = `define( 'ABSPATH', dirname( __FILE__ ) . '\\/' );`;
+ }
+
// WordPress' PHPUnit suite expects a `wp-tests-config.php` in
// the directory that the test suite is contained within.
// Make sure ABSPATH points to the WordPress install.
@@ -106,7 +156,7 @@ async function configureWordPress( environment, config, spinner ) {
[
'sh',
'-c',
- `sed -e "/^require.*wp-settings.php/d" -e "s/define( 'ABSPATH', __DIR__ . '\\/' );/define( 'ABSPATH', '\\/var\\/www\\/html\\/' );\\n\\tdefine( 'WP_DEFAULT_THEME', 'default' );/" /var/www/html/wp-config.php > /wordpress-phpunit/wp-tests-config.php`,
+ `sed -e "/^require.*wp-settings.php/d" -e "s/${ abspathDef }/define( 'ABSPATH', '\\/var\\/www\\/html\\/' );\\n\\tdefine( 'WP_DEFAULT_THEME', 'default' );/" /var/www/html/wp-config.php > /wordpress-phpunit/wp-tests-config.php`,
],
{
config: config.dockerComposeConfigPath,
diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md
index 6eb664984e1e53..9068e1780cc10e 100644
--- a/packages/interactivity/CHANGELOG.md
+++ b/packages/interactivity/CHANGELOG.md
@@ -2,16 +2,30 @@
## Unreleased
+### Bug Fixes
+
+- Fix context inheritance from namespaces different than the current one ([#64677](https://github.com/WordPress/gutenberg/pull/64677)).
+
## 6.5.0 (2024-08-07)
+### Enhancements
+
+- Refactor internal proxy and signals system ([#62734](https://github.com/WordPress/gutenberg/pull/62734)).
+
+### Bug Fixes
+
+- Prevent overriding of existing properties on state and context after a client-side navigation ([#62734](https://github.com/WordPress/gutenberg/pull/62734)).
+
## 6.4.0 (2024-07-24)
## 6.3.0 (2024-07-10)
## 6.2.0 (2024-06-26)
+
### Enhancements
- Export `splitTask` function from `@wordpress/interactivity` package to facilitate yielding to the main thread. See example in [async actions](https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/interactivity-api/api-reference.md#async-actions) documentation. ([#62665](https://github.com/WordPress/gutenberg/pull/62665))
+
## 6.1.0 (2024-06-15)
## 6.0.0 (2024-05-31)
diff --git a/packages/interactivity/src/directives.tsx b/packages/interactivity/src/directives.tsx
index 357fe203399be3..715bc2ea7b16b6 100644
--- a/packages/interactivity/src/directives.tsx
+++ b/packages/interactivity/src/directives.tsx
@@ -273,12 +273,11 @@ export default () => {
const inheritedValue = useContext( inheritedContext );
const ns = defaultEntry!.namespace;
- const currentValue = useRef( {
- [ ns ]: proxifyState( ns, {} ),
- } );
+ const currentValue = useRef( proxifyState( ns, {} ) );
// No change should be made if `defaultEntry` does not exist.
const contextStack = useMemo( () => {
+ const result = { ...inheritedValue };
if ( defaultEntry ) {
const { namespace, value } = defaultEntry;
// Check that the value is a JSON object. Send a console warning if not.
@@ -288,15 +287,16 @@ export default () => {
);
}
updateContext(
- currentValue.current[ namespace ],
+ currentValue.current,
deepClone( value ) as object
);
- currentValue.current[ namespace ] = proxifyContext(
- currentValue.current[ namespace ],
+ currentValue.current = proxifyContext(
+ currentValue.current,
inheritedValue[ namespace ]
);
+ result[ namespace ] = currentValue.current;
}
- return currentValue.current;
+ return result;
}, [ defaultEntry, inheritedValue ] );
return createElement( Provider, { value: contextStack }, children );
diff --git a/packages/patterns/src/components/pattern-overrides-controls.js b/packages/patterns/src/components/pattern-overrides-controls.js
index 3a5ff3e0674a6e..28dd8788a390c6 100644
--- a/packages/patterns/src/components/pattern-overrides-controls.js
+++ b/packages/patterns/src/components/pattern-overrides-controls.js
@@ -2,7 +2,10 @@
* WordPress dependencies
*/
import { useState, useId } from '@wordpress/element';
-import { InspectorControls } from '@wordpress/block-editor';
+import {
+ InspectorControls,
+ privateApis as blockEditorPrivateApis,
+} from '@wordpress/block-editor';
import { BaseControl, Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
@@ -14,22 +17,9 @@ import {
AllowOverridesModal,
DisallowOverridesModal,
} from './allow-overrides-modal';
+import { unlock } from '../lock-unlock';
-function removeBindings( bindings ) {
- let updatedBindings = { ...bindings };
- delete updatedBindings.__default;
- if ( ! Object.keys( updatedBindings ).length ) {
- updatedBindings = undefined;
- }
- return updatedBindings;
-}
-
-function addBindings( bindings ) {
- return {
- ...bindings,
- __default: { source: PATTERN_OVERRIDES_BINDING_SOURCE },
- };
-}
+const { useBlockBindingsUtils } = unlock( blockEditorPrivateApis );
function PatternOverridesControls( {
attributes,
@@ -49,24 +39,22 @@ function PatternOverridesControls( {
const isConnectedToOtherSources =
defaultBindings?.source &&
defaultBindings.source !== PATTERN_OVERRIDES_BINDING_SOURCE;
+ const { updateBlockBindings } = useBlockBindingsUtils();
function updateBindings( isChecked, customName ) {
- const prevBindings = attributes?.metadata?.bindings;
- const updatedBindings = isChecked
- ? addBindings( prevBindings )
- : removeBindings( prevBindings );
-
- const updatedMetadata = {
- ...attributes.metadata,
- bindings: updatedBindings,
- };
-
if ( customName ) {
- updatedMetadata.name = customName;
+ setAttributes( {
+ metadata: {
+ ...attributes.metadata,
+ name: customName,
+ },
+ } );
}
- setAttributes( {
- metadata: updatedMetadata,
+ updateBlockBindings( {
+ __default: isChecked
+ ? { source: PATTERN_OVERRIDES_BINDING_SOURCE }
+ : undefined,
} );
}
@@ -92,6 +80,7 @@ function PatternOverridesControls( {
<>
## Glossary
diff --git a/packages/style-engine/src/index.ts b/packages/style-engine/src/index.ts
index fe5b9929877be7..102d5842e7a0be 100644
--- a/packages/style-engine/src/index.ts
+++ b/packages/style-engine/src/index.ts
@@ -91,3 +91,6 @@ export function getCSSRules(
return rules;
}
+
+// Export style utils.
+export { getCSSValueFromRawStyle } from './styles/utils';
diff --git a/packages/style-engine/src/styles/background/index.ts b/packages/style-engine/src/styles/background/index.ts
index 211b97343d89cc..b943032f9c4417 100644
--- a/packages/style-engine/src/styles/background/index.ts
+++ b/packages/style-engine/src/styles/background/index.ts
@@ -8,6 +8,12 @@ const backgroundImage = {
name: 'backgroundImage',
generate: ( style: Style, options: StyleOptions ) => {
const _backgroundImage = style?.background?.backgroundImage;
+
+ /*
+ * The background image can be a string or an object.
+ * If the background image is a string, it could already contain a url() function,
+ * or have a linear-gradient value.
+ */
if ( typeof _backgroundImage === 'object' && _backgroundImage?.url ) {
return [
{
@@ -21,20 +27,12 @@ const backgroundImage = {
];
}
- /*
- * If the background image is a string, it could already contain a url() function,
- * or have a linear-gradient value.
- */
- if ( typeof _backgroundImage === 'string' ) {
- return generateRule(
- style,
- options,
- [ 'background', 'backgroundImage' ],
- 'backgroundImage'
- );
- }
-
- return [];
+ return generateRule(
+ style,
+ options,
+ [ 'background', 'backgroundImage' ],
+ 'backgroundImage'
+ );
},
};
diff --git a/packages/style-engine/src/styles/test/utils.js b/packages/style-engine/src/styles/test/utils.js
new file mode 100644
index 00000000000000..02f0adf494e71f
--- /dev/null
+++ b/packages/style-engine/src/styles/test/utils.js
@@ -0,0 +1,54 @@
+/**
+ * Internal dependencies
+ */
+import { camelCaseJoin, getCSSValueFromRawStyle, upperFirst } from '../utils';
+
+describe( 'utils', () => {
+ describe( 'upperFirst()', () => {
+ it( 'should return an string with a capitalized first letter', () => {
+ expect( upperFirst( 'toontown' ) ).toEqual( 'Toontown' );
+ } );
+ } );
+
+ describe( 'camelCaseJoin()', () => {
+ it( 'should return a camelCase string', () => {
+ expect( camelCaseJoin( [ 'toon', 'town' ] ) ).toEqual( 'toonTown' );
+ } );
+ } );
+
+ describe( 'getCSSValueFromRawStyle()', () => {
+ it.each( [
+ [ 'min(40%, 400px)', 'min(40%, 400px)' ],
+ [
+ 'var(--wp--preset--color--yellow-bun)',
+ 'var:preset|color|yellow-bun',
+ ],
+ [ 'var(--wp--preset--font-size--h-1)', 'var:preset|font-size|h1' ],
+ [
+ 'var(--wp--preset--font-size--1-px)',
+ 'var:preset|font-size|1px',
+ ],
+ [
+ 'var(--wp--preset--color--orange-11-orange)',
+ 'var:preset|color|orange11orange',
+ ],
+ [
+ 'var(--wp--preset--color--heavenly-blue)',
+ 'var:preset|color|heavenlyBlue',
+ ],
+ [
+ 'var(--wp--preset--background--dark-secrets-100)',
+ 'var:preset|background|dark_Secrets_100',
+ ],
+ [ null, null ],
+ [ false, false ],
+ [ 1000, 1000 ],
+ [ undefined, undefined ],
+ ] )(
+ 'should return %s using an incoming value of %s',
+ ( expected, value ) => {
+ expect( getCSSValueFromRawStyle( value ) ).toEqual( expected );
+ }
+ );
+ } );
+} );
diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts
index 00e9dab8b5892b..74a18d2c2a6e45 100644
--- a/packages/style-engine/src/styles/utils.ts
+++ b/packages/style-engine/src/styles/utils.ts
@@ -61,7 +61,7 @@ export function generateRule(
{
selector: options?.selector,
key: ruleKey,
- value: getCSSVarFromStyleValue( styleValue ),
+ value: getCSSValueFromRawStyle( styleValue ),
},
]
: [];
@@ -103,7 +103,7 @@ export function generateBoxRules(
} else {
const sideRules = individualProperties.reduce(
( acc: GeneratedCSSRule[], side: string ) => {
- const value: string | undefined = getCSSVarFromStyleValue(
+ const value = getCSSValueFromRawStyle(
getStyleValueByPath( boxStyle, [ side ] )
);
if ( value ) {
@@ -127,13 +127,23 @@ export function generateBoxRules(
}
/**
- * Returns a CSS var value from incoming style value following the pattern `var:description|context|slug`.
+ * Returns a WordPress CSS custom var value from incoming style preset value,
+ * if one is detected.
*
- * @param styleValue A raw style value.
+ * The preset value is a string and follows the pattern `var:description|context|slug`.
*
- * @return string A CSS var value.
+ * Example:
+ *
+ * `getCSSValueFromRawStyle( 'var:preset|color|heavenlyBlue' )` // returns 'var(--wp--preset--color--heavenly-blue)'
+ *
+ * @param styleValue A string representing a raw CSS value. Non-strings won't be processed.
+ *
+ * @return A CSS custom var value if the incoming style value is a preset value.
*/
-export function getCSSVarFromStyleValue( styleValue: string ): string {
+
+export function getCSSValueFromRawStyle< StyleValue = string >(
+ styleValue: StyleValue
+): StyleValue {
if (
typeof styleValue === 'string' &&
styleValue.startsWith( VARIABLE_REFERENCE_PREFIX )
@@ -152,7 +162,7 @@ export function getCSSVarFromStyleValue( styleValue: string ): string {
} )
)
.join( VARIABLE_PATH_SEPARATOR_TOKEN_STYLE );
- return `var(--wp--${ variable })`;
+ return `var(--wp--${ variable })` as StyleValue;
}
return styleValue;
}
diff --git a/packages/style-engine/src/test/utils.js b/packages/style-engine/src/test/utils.js
deleted file mode 100644
index 9f1f84d2b45310..00000000000000
--- a/packages/style-engine/src/test/utils.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Internal dependencies
- */
-import {
- camelCaseJoin,
- getCSSVarFromStyleValue,
- upperFirst,
-} from '../styles/utils';
-
-describe( 'utils', () => {
- describe( 'upperFirst()', () => {
- it( 'should return an string with a capitalized first letter', () => {
- expect( upperFirst( 'toontown' ) ).toEqual( 'Toontown' );
- } );
- } );
-
- describe( 'camelCaseJoin()', () => {
- it( 'should return a camelCase string', () => {
- expect( camelCaseJoin( [ 'toon', 'town' ] ) ).toEqual( 'toonTown' );
- } );
- } );
-
- describe( 'getCSSVarFromStyleValue()', () => {
- it( 'should return a compiled CSS var', () => {
- expect(
- getCSSVarFromStyleValue( 'var:preset|color|yellow-bun' )
- ).toEqual( 'var(--wp--preset--color--yellow-bun)' );
- } );
-
- it( 'should kebab case numbers', () => {
- expect(
- getCSSVarFromStyleValue( 'var:preset|font-size|h1' )
- ).toEqual( 'var(--wp--preset--font-size--h-1)' );
- } );
-
- it( 'should kebab case numbers as prefix', () => {
- expect(
- getCSSVarFromStyleValue( 'var:preset|font-size|1px' )
- ).toEqual( 'var(--wp--preset--font-size--1-px)' );
- } );
-
- it( 'should kebab case both sides of numbers', () => {
- expect(
- getCSSVarFromStyleValue( 'var:preset|color|orange11orange' )
- ).toEqual( 'var(--wp--preset--color--orange-11-orange)' );
- } );
-
- it( 'should kebab case camel case', () => {
- expect(
- getCSSVarFromStyleValue( 'var:preset|color|heavenlyBlue' )
- ).toEqual( 'var(--wp--preset--color--heavenly-blue)' );
- } );
-
- it( 'should kebab case underscores', () => {
- expect(
- getCSSVarFromStyleValue(
- 'var:preset|background|dark_Secrets_100'
- )
- ).toEqual( 'var(--wp--preset--background--dark-secrets-100)' );
- } );
- } );
-} );
diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts
index 5b361836a8e375..8de623fcfcb2bf 100644
--- a/packages/style-engine/src/types.ts
+++ b/packages/style-engine/src/types.ts
@@ -94,7 +94,7 @@ export interface StyleOptions {
export interface GeneratedCSSRule {
selector?: string;
- value: string;
+ value: string | unknown;
/**
* The CSS key in JS style attribute format, compatible with React.
* E.g. `paddingTop` instead of `padding-top`.
diff --git a/packages/widgets/src/blocks/legacy-widget/edit/widget-type-selector.js b/packages/widgets/src/blocks/legacy-widget/edit/widget-type-selector.js
index 215e2d2447c3c9..cc7c0830a319bf 100644
--- a/packages/widgets/src/blocks/legacy-widget/edit/widget-type-selector.js
+++ b/packages/widgets/src/blocks/legacy-widget/edit/widget-type-selector.js
@@ -27,6 +27,8 @@ export default function WidgetTypeSelector( { selectedId, onSelect } ) {
return (
array(
'backgroundImage' => array(
- 'url' => 'https://example.com/image.jpg',
- 'source' => 'file',
+ 'url' => 'https://example.com/image.jpg',
),
),
'expected_wrapper' => '