diff --git a/.eslintrc.js b/.eslintrc.js
index d9f2b3ae8e132..5e20681906c42 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -54,6 +54,29 @@ module.exports = {
},
],
'@wordpress/no-unsafe-wp-apis': 'off',
+ 'no-restricted-imports': [
+ 'error',
+ {
+ paths: [
+ {
+ name: 'lodash',
+ importNames: [ 'memoize' ],
+ message: 'Please use `memize` instead.',
+ },
+ {
+ name: 'reakit',
+ message:
+ 'Please use Reakit API through `@wordpress/components` instead.',
+ },
+ {
+ name: 'redux',
+ importNames: [ 'combineReducers' ],
+ message:
+ 'Please use `combineReducers` from `@wordpress/data` instead.',
+ },
+ ],
+ },
+ ],
'no-restricted-syntax': [
'error',
// NOTE: We can't include the forward slash in our regex or
@@ -79,16 +102,6 @@ module.exports = {
message:
'Deprecated functions must be removed before releasing this version.',
},
- {
- selector:
- 'ImportDeclaration[source.value="redux"] Identifier.imported[name="combineReducers"]',
- message: 'Use `combineReducers` from `@wordpress/data`',
- },
- {
- selector:
- 'ImportDeclaration[source.value="lodash"] Identifier.imported[name="memoize"]',
- message: 'Use memize instead of Lodash’s memoize',
- },
{
selector:
'CallExpression[callee.object.name="page"][callee.property.name="waitFor"]',
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 033ecb9d331c0..6a2c29e06c81e 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -11,7 +11,7 @@
/packages/data-controls @nerrad
# Blocks
-/packages/block-library @ajitbohra @talldan
+/packages/block-library @ajitbohra
/packages/block-library/src/gallery @mkevins
/packages/block-library/src/social-links @mkaz
/packages/block-library/src/social-link @mkaz
@@ -54,7 +54,7 @@
/packages/custom-templated-path-webpack-plugin @ntwb @nerrad @ajitbohra
/packages/docgen @nosolosw
/packages/e2e-test-utils @gziolo @ntwb @nerrad @ajitbohra
-/packages/e2e-tests @ntwb @nerrad @ajitbohra @talldan
+/packages/e2e-tests @ntwb @nerrad @ajitbohra
/packages/eslint-plugin @gziolo @ntwb @nerrad @ajitbohra
/packages/jest-console @gziolo @ntwb @nerrad @ajitbohra
/packages/jest-preset-default @gziolo @ntwb @nerrad @ajitbohra
@@ -88,10 +88,10 @@
/packages/html-entities
/packages/i18n @swissspidy
/packages/is-shallow-equal
-/packages/keycodes @talldan @ellatrix
+/packages/keycodes @ellatrix
/packages/priority-queue
/packages/token-list
-/packages/url @talldan
+/packages/url
/packages/wordcount
/packages/warning
/packages/keyboard-shortcuts
@@ -116,6 +116,9 @@
/lib @timothybjacobs @spacedmonkey
/lib/global-styles.php @timothybjabocs @spacedmonkey @nosolosw
/lib/experimental-default-theme.json @timothybjabocs @spacedmonkey @nosolosw
+/lib/class-wp-theme-json.php @timothybjabocs @spacedmonkey @nosolosw
+/lib/class-wp-theme-json-resolver.php @timothybjabocs @spacedmonkey @nosolosw
+/phpunit/class-wp-theme-json-test.php @nosolosw
# Native (Unowned)
*.native.js @ghost
diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md
index 050d91e2ef001..a27a295da5089 100644
--- a/.github/ISSUE_TEMPLATE/Bug_report.md
+++ b/.github/ISSUE_TEMPLATE/Bug_report.md
@@ -1,40 +1,68 @@
---
name: Bug report
-about: Create a report to help us improve
+about: Report a bug with the WordPress block editor or Gutenberg plugin
---
-**Describe the bug**
-A clear and concise description of what the bug is.
+
+
+## Description
+
+
+## Step-by-step reproduction instructions
+
+
+## Expected behaviour
+
+
+## Actual behaviour
+
+
+## Screenshots or screen recording (optional)
+
+
+## Code snippet (optional)
+
+
+## WordPress information
+- WordPress version:
+- Gutenberg version:
+- Are all plugins except Gutenberg deactivated?
+- Are you using a default theme (e.g. Twenty Twenty-One)?
+
+## Device information
+- Device:
+- Operating system:
+- Browser:
diff --git a/.github/ISSUE_TEMPLATE/Bug_report_mobile.md b/.github/ISSUE_TEMPLATE/Bug_report_mobile.md
new file mode 100644
index 0000000000000..295e8a0ed6b0a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/Bug_report_mobile.md
@@ -0,0 +1,55 @@
+---
+name: Bug report (Mobile)
+about: Report a bug with the mobile app version of Gutenberg
+labels: Mobile App Android/iOS
+
+---
+
+
+
+## Description
+
+
+## Step-by-step reproduction instructions
+
+
+## Expected behaviour
+
+
+## Actual behaviour
+
+
+## Screenshots or screen recording (optional)
+
+
+## WordPress information
+- WordPress version:
+- Gutenberg version:
+- Are all plugins except Gutenberg deactivated?
+- Are you using a default theme (e.g. Twenty Twenty-One)?
+
+## Device information
+- Device:
+- Operating system:
+- WordPress app version:
diff --git a/.github/ISSUE_TEMPLATE/Custom.md b/.github/ISSUE_TEMPLATE/Custom.md
deleted file mode 100644
index c621fa8402fc4..0000000000000
--- a/.github/ISSUE_TEMPLATE/Custom.md
+++ /dev/null
@@ -1,17 +0,0 @@
----
-name: Help Request
-about: Please post help requests or ‘how to’ questions in support channels first
-
----
-
-Search first! Your issue may have already been reported.
-
-For general help requests, please post in the support forum at https://wordpress.org/support/forum/how-to-and-troubleshooting/.
-
-Technical help requests have their own section of the support forum at https://wordpress.org/support/forum/wp-advanced/.
-
-You may also ask for technical support at https://wordpress.stackexchange.com/.
-
-Please make sure you have checked the Handbook at https://wordpress.org/gutenberg/handbook before asking your question.
-
-Thank you!
diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md
index e36eff9baf5d2..cfae99f42ff9e 100644
--- a/.github/ISSUE_TEMPLATE/Feature_request.md
+++ b/.github/ISSUE_TEMPLATE/Feature_request.md
@@ -1,14 +1,18 @@
---
name: Feature request
-about: Suggest an idea for this project
+about: Propose an idea for a feature or an enhancement
---
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+## What problem does this address?
+
-**Describe the solution you'd like**
-A clear and concise description of what you want to happen.
-
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
+## What is your proposed solution?
+
diff --git a/.github/ISSUE_TEMPLATE/Mobile_apps_bug_report.md b/.github/ISSUE_TEMPLATE/Mobile_apps_bug_report.md
deleted file mode 100644
index 8581315fe6c8a..0000000000000
--- a/.github/ISSUE_TEMPLATE/Mobile_apps_bug_report.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-name: Bug report for Mobile Apps
-about: Create a report to help us improve the Gutenberg mobile apps version
-labels: Mobile App Android/iOS
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Tap on '....'
-3. Scroll down to '....'
-4. See '... exact error ...'
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**Editor version (please complete the following information):**
-- WordPress version: [e.g: 5.3.2]
-- Does the website have the Gutenberg plugin installed, or is it using the block editor that comes by default? [e.g: "gutenberg plugin", "default"]
-- If the Gutenberg plugin is installed, which version is it? [e.g., 7.6]
-
-**Smartphone (please complete the following information):**
- - Device: [e.g. iPhone6, Pixel 3]
- - OS: [e.g. iOS 8.1, Android 10.0]
- - WordPress App Version [e.g. 15.3] or Demo App Version [branch name or git commit hash]
-
-**Additional context**
-- To report a security issue, please visit the WordPress HackerOne program: https://hackerone.com/wordpress.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000000..65b1db3f9f9ef
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: false
+contact_links:
+ - name: General help request
+ url: https://wordpress.org/support/forum/how-to-and-troubleshooting/
+ about: For general help requests, create a new topic in the Fixing WordPress support forum
+ - name: Technical help request
+ url: https://wordpress.org/support/forum/wp-advanced/
+ about: For more technical help requests, create a new topic in the Developing with WordPress Forum
+ - name: Development help request
+ url: https://wordpress.stackexchange.com/
+ about: For questions about WordPress development, ask a question in the WordPress Development Stack Exchange
diff --git a/.github/workflows/stale-issue-needs-info.yml b/.github/workflows/stale-issue-needs-info.yml
new file mode 100644
index 0000000000000..c7413a32c2e4e
--- /dev/null
+++ b/.github/workflows/stale-issue-needs-info.yml
@@ -0,0 +1,18 @@
+name: "Close stale issues that requires info"
+on:
+ schedule:
+ - cron: "30 1 * * *"
+
+jobs:
+ stale:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/stale@v3
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ stale-issue-message: 'Help us move this issue forward. Since it has no activity after 15 days of requesting more information, a bot is marking the issue as stale. Please add additional information as a comment or this issued will be closed in 5 days.'
+ close-issue-message: 'This issue was closed because more information was requested and there was no activity. If this is a bug report and still a problem, please supply the additional information requested and reopen the issue.'
+ days-before-stale: 15
+ days-before-close: 5
+ only-labels: '[Status] Needs More Info'
+ stale-issue-label: '[Status] Stale'
diff --git a/bin/plugin/commands/packages.js b/bin/plugin/commands/packages.js
index 6291008ae575a..7625bb8a71e27 100644
--- a/bin/plugin/commands/packages.js
+++ b/bin/plugin/commands/packages.js
@@ -108,8 +108,18 @@ async function updatePackages(
const changelogFiles = await glob(
path.resolve( gitWorkingDirectoryPath, 'packages/*/CHANGELOG.md' )
);
+ const changelogFilesPublicPackages = changelogFiles.filter(
+ ( changelogPath ) => {
+ const pkg = require( path.join(
+ path.dirname( changelogPath ),
+ 'package.json'
+ ) );
+ return pkg.private !== true;
+ }
+ );
+
const processedPackages = await Promise.all(
- changelogFiles.map( async ( changelogPath ) => {
+ changelogFilesPublicPackages.map( async ( changelogPath ) => {
const fileStream = fs.createReadStream( changelogPath );
const rl = readline.createInterface( {
diff --git a/changelog.txt b/changelog.txt
index c335fd3bb0e1e..09218013c9007 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,159 @@
== Changelog ==
+= 9.8.0-rc.1 =
+
+### Enhancements
+
+- Try: Transparent spacer. ([28103](https://github.com/WordPress/gutenberg/pull/28103))
+- Fix the border radius in the site editor. ([27986](https://github.com/WordPress/gutenberg/pull/27986))
+- Display matching variation icon in Block Switcher. ([27903](https://github.com/WordPress/gutenberg/pull/27903))
+- Group Block: Add border radius. ([27665](https://github.com/WordPress/gutenberg/pull/27665))
+- Try: Fix appender margins again. ([27392](https://github.com/WordPress/gutenberg/pull/27392))
+
+### New APIs
+
+- Create Block: Allow using locally installed packages with templates. ([28105](https://github.com/WordPress/gutenberg/pull/28105))
+- Create block: Add support for static assets. ([28038](https://github.com/WordPress/gutenberg/pull/28038))
+- Compose: Export useIsomorphicLayoutEffect and use it. ([28023](https://github.com/WordPress/gutenberg/pull/28023))
+
+### Bug Fixes
+
+- Fix block error when transforming blocks with Link Popover opened. ([28136](https://github.com/WordPress/gutenberg/pull/28136))
+- Fix PHP Notice in navigation-link. ([28134](https://github.com/WordPress/gutenberg/pull/28134))
+- Prevent link paste in RichText components in Button and Navigation blocks. ([28130](https://github.com/WordPress/gutenberg/pull/28130))
+- Fix floating date status inferred for posts where the status has been edited. ([28127](https://github.com/WordPress/gutenberg/pull/28127))
+- Show an error message when a reusable block has gone missing. ([28126](https://github.com/WordPress/gutenberg/pull/28126))
+- BlockSwitcher: Fix crash due to null reference. ([28122](https://github.com/WordPress/gutenberg/pull/28122))
+- Fix nested cover block bug. ([28114](https://github.com/WordPress/gutenberg/pull/28114))
+- Verse: Fix line-wrap rendering on front-end of site. ([28109](https://github.com/WordPress/gutenberg/pull/28109))
+- FocalPointPicker: Fix rendering and dragging experience. ([28096](https://github.com/WordPress/gutenberg/pull/28096))
+- Fix invalid cover block transforms. ([28087](https://github.com/WordPress/gutenberg/pull/28087))
+- Block Directory: Fix "missing" block when the block can be installed from the directory. ([28030](https://github.com/WordPress/gutenberg/pull/28030))
+- Reusable blocks: Fix dismiss notice after error. ([28015](https://github.com/WordPress/gutenberg/pull/28015))
+- Fix locked template not updating when inner blocks template prop changes. ([28007](https://github.com/WordPress/gutenberg/pull/28007))
+- Fix editor crash when registering a block pattern without `categories`. ([27970](https://github.com/WordPress/gutenberg/pull/27970))
+- Fix the RTL editor styles and the theme styles option. ([27947](https://github.com/WordPress/gutenberg/pull/27947))
+- Don't close the block inserter when clicking the scrollbar or an empty area. ([27946](https://github.com/WordPress/gutenberg/pull/27946))
+- Fix AlignmentMatrixControl focus issue. ([27945](https://github.com/WordPress/gutenberg/pull/27945))
+- Fix unexpected autosave for published posts. ([27942](https://github.com/WordPress/gutenberg/pull/27942))
+- Fix RadioGroup to support zero as a Radio value. ([27906](https://github.com/WordPress/gutenberg/pull/27906))
+- Update embed block transforms to permit multiple links to be pasted in a paragraph (#27551). ([27746](https://github.com/WordPress/gutenberg/pull/27746))
+- Change the week header and left button style to meet the date spacing. ([27730](https://github.com/WordPress/gutenberg/pull/27730))
+- Add aria labels to box control component inputs/button. ([27727](https://github.com/WordPress/gutenberg/pull/27727))
+- Use clientWidth when no width is available for cropper. ([27687](https://github.com/WordPress/gutenberg/pull/27687))
+- Core Data: Normalize `_fields` value for use in `stableKey`. ([27526](https://github.com/WordPress/gutenberg/pull/27526))
+
+### Performance
+
+- Components: Expose composite API from Reakit. ([28085](https://github.com/WordPress/gutenberg/pull/28085))
+- Improve Inserter block hover performance. ([26348](https://github.com/WordPress/gutenberg/pull/26348))
+
+### Experiments
+
+- Delete unused options while upgrading the plugin. ([28164](https://github.com/WordPress/gutenberg/pull/28164))
+- Hide the theme without comments.php deprecation message. ([28128](https://github.com/WordPress/gutenberg/pull/28128))
+- Fix navigation editor. ([28080](https://github.com/WordPress/gutenberg/pull/28080))
+- Widgets: Temporary fix for saving widgets. ([28078](https://github.com/WordPress/gutenberg/pull/28078))
+- Decouple query from edit site. ([27972](https://github.com/WordPress/gutenberg/pull/27972))
+- Only enable the template mode for viewable post types. ([27948](https://github.com/WordPress/gutenberg/pull/27948))
+- Box control units: Ensure custom units are preserved. ([27800](https://github.com/WordPress/gutenberg/pull/27800))
+- Theme JSON: Add border radius to the theme styles schema. ([27791](https://github.com/WordPress/gutenberg/pull/27791))
+- Add theme.json i18n mechanism and JSON file specifying which theme.json paths are translatable. ([27380](https://github.com/WordPress/gutenberg/pull/27380))
+- Navigation Block: Use draft status when user creates a post and don't render unpublished posts in menus. ([27207](https://github.com/WordPress/gutenberg/pull/27207))
+- Full site editor: Load content in iframe. ([25775](https://github.com/WordPress/gutenberg/pull/25775))
+
+### Documentation
+
+- Docs: Improve README file for `@wordpress/create-block`. ([28052](https://github.com/WordPress/gutenberg/pull/28052))
+- Create Block: Update the demo included in the README file. ([28037](https://github.com/WordPress/gutenberg/pull/28037))
+- Docs: Switch heading to Quick Start for consistency. ([28019](https://github.com/WordPress/gutenberg/pull/28019))
+- Docs: A wether, as it turns out, is a castrated ram. ([28008](https://github.com/WordPress/gutenberg/pull/28008))
+- Update Quickstart guide for the Development Environment documentation. ([28005](https://github.com/WordPress/gutenberg/pull/28005))
+- Update copyright year to 2021 in `license.md`. ([27951](https://github.com/WordPress/gutenberg/pull/27951))
+- Block API: Add more inline comments. ([20257](https://github.com/WordPress/gutenberg/pull/20257))
+
+### Code Quality
+
+- Remove effects test file and remove unused refx dependency. ([28162](https://github.com/WordPress/gutenberg/pull/28162))
+- Annotations: Replace store name string with exposed store definition. ([28156](https://github.com/WordPress/gutenberg/pull/28156))
+- Edit Widgets: Replace store name string with exposed store definition. ([28044](https://github.com/WordPress/gutenberg/pull/28044))
+- Interface: Replace store name string with exposed store definition. ([28041](https://github.com/WordPress/gutenberg/pull/28041))
+- Upgrade Reakit to version 1.3.4. ([28013](https://github.com/WordPress/gutenberg/pull/28013))
+- Fix PHPCS warning: Undefined variable $i. ([27955](https://github.com/WordPress/gutenberg/pull/27955))
+- Consolidate block editor initializations. ([27954](https://github.com/WordPress/gutenberg/pull/27954))
+- Fix create-block PHP template files according to WordPress standards. ([27949](https://github.com/WordPress/gutenberg/pull/27949))
+- block-directory: Simplify the LOAD_ASSETS flow by making it an async function. ([25956](https://github.com/WordPress/gutenberg/pull/25956))
+
+### Tools
+
+- Scripts: Align default engines for `check-engines` with the package. ([28143](https://github.com/WordPress/gutenberg/pull/28143))
+- end-to-end FSE: Fix intermittent errors in multi entity editing test. ([28107](https://github.com/WordPress/gutenberg/pull/28107))
+- Testing: Prevent a direct usage of Reakit. ([28095](https://github.com/WordPress/gutenberg/pull/28095))
+- Update changelog for stylelint-config. ([28074](https://github.com/WordPress/gutenberg/pull/28074))
+- Testing: Fix randomly failing end-to-end test. ([28073](https://github.com/WordPress/gutenberg/pull/28073))
+- Upgrade puppeteer to 5.5.0. ([28055](https://github.com/WordPress/gutenberg/pull/28055))
+- Build Plugin Workflow: Bump node version to 14. ([28048](https://github.com/WordPress/gutenberg/pull/28048))
+- GH Actions: Compare Performance upon Release. ([28046](https://github.com/WordPress/gutenberg/pull/28046))
+- Scripts: Add support for static assets in build commands. ([28043](https://github.com/WordPress/gutenberg/pull/28043))
+- Performance tests: Fix. ([28026](https://github.com/WordPress/gutenberg/pull/28026))
+- Scripts: Make it possible to transpile `.jsx` files with build command. ([28002](https://github.com/WordPress/gutenberg/pull/28002))
+- Revert "Upgrade webpack to version 5". ([27974](https://github.com/WordPress/gutenberg/pull/27974))
+- Scripts: ESLint minor version upgrade to 7.17.0. ([27965](https://github.com/WordPress/gutenberg/pull/27965))
+- Scripts: Upgrade Jest to the new major version (26.x). ([27956](https://github.com/WordPress/gutenberg/pull/27956))
+- Update the minimum Node.js version to 12. ([27934](https://github.com/WordPress/gutenberg/pull/27934))
+- wp-env: Ensure the environment is used with the logs command. ([27907](https://github.com/WordPress/gutenberg/pull/27907))
+- Use @wordpress/stylelint-config in @wordpress/scripts. ([27810](https://github.com/WordPress/gutenberg/pull/27810))
+- GH Actions: Add action to upload release to SVN repo. ([27591](https://github.com/WordPress/gutenberg/pull/27591))
+- GitHub Actions: Create Release Draft when tagging version. ([27488](https://github.com/WordPress/gutenberg/pull/27488))
+- ESLint Plugin: Enable import rules used in Gutenberg. ([27387](https://github.com/WordPress/gutenberg/pull/27387))
+- Eslint: Add no-unsafe-wp-apis to recommended configuration. ([27327](https://github.com/WordPress/gutenberg/pull/27327))
+- Upgrade webpack to version 5. ([26382](https://github.com/WordPress/gutenberg/pull/26382))
+- Remove /wordpress from test/linting ignore paths. ([20270](https://github.com/WordPress/gutenberg/pull/20270))
+
+### Various
+
+- URL: Remove redundant array coercion. ([28072](https://github.com/WordPress/gutenberg/pull/28072))
+- Add: Save time theme.json escaping. ([28061](https://github.com/WordPress/gutenberg/pull/28061))
+- Visual editor: Remove focusable wrapper. ([28058](https://github.com/WordPress/gutenberg/pull/28058))
+- Readme: Increase tested Version up to WP 5.6. ([28050](https://github.com/WordPress/gutenberg/pull/28050))
+- Interface: Remove deprecated prop from InterfaceSkeleton. ([28034](https://github.com/WordPress/gutenberg/pull/28034))
+- List View: Reduce whitespace and always show nested blocks. ([28029](https://github.com/WordPress/gutenberg/pull/28029))
+- Making the sidebar inspector's tabs stick when scrolling. ([28003](https://github.com/WordPress/gutenberg/pull/28003))
+- Chore: Update Lerna dependency. ([27990](https://github.com/WordPress/gutenberg/pull/27990))
+- Try: Make focus width a CSS variable. ([27968](https://github.com/WordPress/gutenberg/pull/27968))
+- Add translation context to all block's titles. ([27933](https://github.com/WordPress/gutenberg/pull/27933))
+- Avoid using auto-drafts for theme templates and template parts. ([27910](https://github.com/WordPress/gutenberg/pull/27910))
+- Add primary destructive button style. ([27774](https://github.com/WordPress/gutenberg/pull/27774))
+- Modifies the widgets dashboard link to point to the new widgets editor. ([26880](https://github.com/WordPress/gutenberg/pull/26880))
+- Use standard select element for small number of authors. ([26426](https://github.com/WordPress/gutenberg/pull/26426))
+- Add srcset for cover image. ([25171](https://github.com/WordPress/gutenberg/pull/25171))
+
+
+= 9.7.3 =
+
+### Bug Fixes
+
+- Prevent mangle of translation functions to fix RTL locales.
+
+
+= 9.7.2 =
+
+### Bug Fixes
+
+- Keep the inserter opened when clicking the scrollbar.
+
+### Various
+
+- Updated the "tested up to" WordPress version.
+
+
+= 9.7.1 =
+
+### Bug Fixes
+
+ - Fix styling of the verse block.
+
+
= 9.7.0 =
### Features
diff --git a/docs/manifest.json b/docs/manifest.json
index 8c0881895f142..fc5de0f2c6377 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -1469,6 +1469,12 @@
"markdown_source": "../packages/core-data/README.md",
"parent": "packages"
},
+ {
+ "title": "@wordpress/create-block-tutorial-template",
+ "slug": "packages-create-block-tutorial-template",
+ "markdown_source": "../packages/create-block-tutorial-template/README.md",
+ "parent": "packages"
+ },
{
"title": "@wordpress/create-block",
"slug": "packages-create-block",
diff --git a/gutenberg.php b/gutenberg.php
index 1f4b7c40bc37a..cd5311ad3db31 100644
--- a/gutenberg.php
+++ b/gutenberg.php
@@ -5,7 +5,7 @@
* Description: Printing since 1440. This is the development plugin for the new block editor in core.
* Requires at least: 5.3
* Requires PHP: 5.6
- * Version: 9.7.0
+ * Version: 9.8.0-rc.1
* Author: Gutenberg Team
* Text Domain: gutenberg
*
diff --git a/lib/class-wp-theme-json-resolver.php b/lib/class-wp-theme-json-resolver.php
index a94dbb1fde86d..721aa77b04629 100644
--- a/lib/class-wp-theme-json-resolver.php
+++ b/lib/class-wp-theme-json-resolver.php
@@ -71,6 +71,85 @@ private static function get_from_file( $file_path ) {
return $config;
}
+ /**
+ * Processes a tree from i18n-theme.json into a linear array
+ * containing the a translatable path from theme.json and an array
+ * of properties that are translatable.
+ *
+ * @param array $file_structure_partial A part of a theme.json i18n tree.
+ * @param array $current_path An array with a path on the theme.json i18n tree.
+ *
+ * @return array An array of arrays each one containing a translatable path and an array of properties that are translatable.
+ */
+ private static function theme_json_i18_file_structure_to_preset_paths( $file_structure_partial, $current_path = array() ) {
+ $result = array();
+ foreach ( $file_structure_partial as $property => $partial_child ) {
+ if ( is_numeric( $property ) ) {
+ return array(
+ array(
+ 'path' => $current_path,
+ 'translatable_keys' => $file_structure_partial,
+ ),
+ );
+ }
+ $result = array_merge(
+ $result,
+ self::theme_json_i18_file_structure_to_preset_paths( $partial_child, array_merge( $current_path, array( $property ) ) )
+ );
+ }
+ return $result;
+ }
+
+ /**
+ * Returns a data structure used in theme.json translation.
+ *
+ * @return array An array of theme.json paths that are translatable and the keys that are translatable
+ */
+ private static function get_presets_to_translate() {
+ static $theme_json_i18n = null;
+ if ( null === $theme_json_i18n ) {
+ $file_structure = self::get_from_file( __DIR__ . '/experimental-i18n-theme.json' );
+ $theme_json_i18n = self::theme_json_i18_file_structure_to_preset_paths( $file_structure );
+
+ }
+ return $theme_json_i18n;
+ }
+
+ /**
+ * Translates a theme.json structure.
+ *
+ * @param array $theme_json_structure A theme.json structure that is going to be translatable.
+ * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings.
+ * Default 'default'.
+ */
+ private static function translate_presets( &$theme_json_structure, $domain = 'default' ) {
+ $preset_to_translate = self::get_presets_to_translate();
+ foreach ( $theme_json_structure as &$context_value ) {
+ if ( empty( $context_value ) ) {
+ continue;
+ }
+ foreach ( $preset_to_translate as $preset ) {
+ $path = $preset['path'];
+ $translatable_keys = $preset['translatable_keys'];
+ $array_to_translate = gutenberg_experimental_get( $context_value, $path, null );
+ if ( null === $array_to_translate ) {
+ continue;
+ }
+ foreach ( $array_to_translate as &$item_to_translate ) {
+ foreach ( $translatable_keys as $translatable_key ) {
+ if ( empty( $item_to_translate[ $translatable_key ] ) ) {
+ continue;
+ }
+ // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain
+ $item_to_translate[ $translatable_key ] = translate( $item_to_translate[ $translatable_key ], $domain );
+ // phpcs:enable
+ }
+ }
+ gutenberg_experimental_set( $context_value, $path, $array_to_translate );
+ }
+ }
+ }
+
/**
* Return core's origin config.
*
@@ -82,6 +161,7 @@ private static function get_core_origin() {
}
$config = self::get_from_file( __DIR__ . '/experimental-default-theme.json' );
+ self::translate_presets( $config );
// Start i18n logic to remove when JSON i18 strings are extracted.
$default_colors_i18n = array(
@@ -155,6 +235,7 @@ private static function get_core_origin() {
*/
private function get_theme_origin( $theme_support_data = array() ) {
$theme_json_data = self::get_from_file( locate_template( 'experimental-theme.json' ) );
+ self::translate_presets( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) );
/*
* We want the presets and settings declared in theme.json
@@ -233,11 +314,18 @@ private static function get_user_origin() {
return $config;
}
- if ( is_array( $decoded_data ) ) {
+ // Very important to verify if the flag isGlobalStylesUserThemeJSON is true.
+ // If is not true the content was not escaped and is not safe.
+ if (
+ is_array( $decoded_data ) &&
+ isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
+ $decoded_data['isGlobalStylesUserThemeJSON']
+ ) {
+ unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
$config = $decoded_data;
}
}
- self::$user = new WP_Theme_JSON( $config, true );
+ self::$user = new WP_Theme_JSON( $config );
return self::$user;
}
diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php
index 6b140156f65db..0784bd2185e59 100644
--- a/lib/class-wp-theme-json.php
+++ b/lib/class-wp-theme-json.php
@@ -293,10 +293,9 @@ class WP_Theme_JSON {
/**
* Constructor.
*
- * @param array $contexts A structure that follows the theme.json schema.
- * @param boolean $should_escape_styles Whether the incoming styles should be escaped.
+ * @param array $contexts A structure that follows the theme.json schema.
*/
- public function __construct( $contexts = array(), $should_escape_styles = false ) {
+ public function __construct( $contexts = array() ) {
$this->contexts = array();
if ( ! is_array( $contexts ) ) {
@@ -317,10 +316,10 @@ public function __construct( $contexts = array(), $should_escape_styles = false
// Process styles subtree.
$this->process_key( 'styles', $context, self::SCHEMA );
if ( isset( $context['styles'] ) ) {
- $this->process_key( 'border', $context['styles'], self::SCHEMA['styles'], $should_escape_styles );
- $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'], $should_escape_styles );
- $this->process_key( 'spacing', $context['styles'], self::SCHEMA['styles'], $should_escape_styles );
- $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'], $should_escape_styles );
+ $this->process_key( 'border', $context['styles'], self::SCHEMA['styles'] );
+ $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'] );
+ $this->process_key( 'spacing', $context['styles'], self::SCHEMA['styles'] );
+ $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'] );
if ( empty( $context['styles'] ) ) {
unset( $context['styles'] );
@@ -346,6 +345,64 @@ public function __construct( $contexts = array(), $should_escape_styles = false
}
}
+ /**
+ * Returns the kebab-cased name of a given property.
+ *
+ * @param string $property Property name to convert.
+ * @return string kebab-cased name of the property
+ */
+ private static function to_kebab_case( $property ) {
+ $mappings = self::get_case_mappings();
+ return $mappings['to_kebab_case'][ $property ];
+ }
+
+ /**
+ * Returns the property name of a kebab-cased property.
+ *
+ * @param string $property Property name to convert in kebab-case.
+ * @return string Name of the property
+ */
+ private static function to_property( $property ) {
+ $mappings = self::get_case_mappings();
+ return $mappings['to_property'][ $property ];
+ }
+
+ /**
+ * Returns a mapping on metadata properties to avoid having to constantly
+ * transforms properties between camel case and kebab.
+ *
+ * @return array Containing three mappings
+ * "to_kebab_case" mapping properties in camel case to
+ * properties in kebab case e.g: "paddingTop" to "padding-top".
+ * "to_property" mapping properties in kebab case to
+ * the main properties in camel case e.g: "padding-top" to "padding".
+ */
+ private static function get_case_mappings() {
+ static $case_mappings;
+ if ( null === $case_mappings ) {
+ $case_mappings = array(
+ 'to_kebab_case' => array(),
+ 'to_property' => array(),
+ );
+ foreach ( self::PROPERTIES_METADATA as $key => $metadata ) {
+ $kebab_case = strtolower( preg_replace( '/(? $value ) {
- $name = 'background-color';
- if ( 'gradient' === $property ) {
- $name = 'background';
- }
-
- if ( is_array( $value ) ) {
- $result = array();
- foreach ( $value as $subproperty => $subvalue ) {
- $result_subproperty = safecss_filter_attr( "$name: $subvalue" );
- if ( '' !== $result_subproperty ) {
- $result[ $subproperty ] = $result_subproperty;
- }
- }
-
- if ( empty( $result ) ) {
- unset( $input[ $key ][ $property ] );
- }
- } else {
- $result = safecss_filter_attr( "$name: $value" );
-
- if ( '' === $result ) {
- unset( $input[ $key ][ $property ] );
- }
- }
- }
- }
-
if ( 0 === count( $input[ $key ] ) ) {
unset( $input[ $key ] );
}
@@ -669,7 +695,6 @@ private static function compute_style_properties( &$declarations, $context, $con
if ( empty( $context['styles'] ) ) {
return;
}
-
$properties = array();
foreach ( self::PROPERTIES_METADATA as $name => $metadata ) {
if ( ! in_array( $name, $context_supports, true ) ) {
@@ -696,9 +721,9 @@ private static function compute_style_properties( &$declarations, $context, $con
foreach ( $properties as $prop ) {
$value = self::get_property_value( $context['styles'], $prop['value'] );
if ( ! empty( $value ) ) {
- $kebabcased_name = strtolower( preg_replace( '/(? $kebabcased_name,
+ $kebab_cased_name = self::to_kebab_case( $prop['name'] );
+ $declarations[] = array(
+ 'name' => $kebab_cased_name,
'value' => $value,
);
}
@@ -1015,6 +1040,119 @@ public function merge( $theme_json ) {
}
}
+ /**
+ * Removes insecure data from theme.json.
+ */
+ public function remove_insecure_properties() {
+ $blocks_metadata = self::get_blocks_metadata();
+ foreach ( $this->contexts as $context_name => &$context ) {
+ // Escape the context key.
+ if ( empty( $blocks_metadata[ $context_name ] ) ) {
+ unset( $this->contexts[ $context_name ] );
+ continue;
+ }
+
+ $escaped_settings = null;
+ $escaped_styles = null;
+
+ // Style escaping.
+ if ( ! empty( $context['styles'] ) ) {
+ $supports = $blocks_metadata[ $context_name ]['supports'];
+ $declarations = array();
+ self::compute_style_properties( $declarations, $context, $supports );
+ foreach ( $declarations as $declaration ) {
+ $style_to_validate = $declaration['name'] . ': ' . $declaration['value'];
+ if ( esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate ) {
+ if ( null === $escaped_styles ) {
+ $escaped_styles = array();
+ }
+ $property = self::to_property( $declaration['name'] );
+ $path = self::PROPERTIES_METADATA[ $property ]['value'];
+ if ( self::has_properties( self::PROPERTIES_METADATA[ $property ] ) ) {
+ $declaration_divided = explode( '-', $declaration['name'] );
+ $path[] = $declaration_divided[1];
+ gutenberg_experimental_set(
+ $escaped_styles,
+ $path,
+ gutenberg_experimental_get( $context['styles'], $path )
+ );
+ } else {
+ gutenberg_experimental_set(
+ $escaped_styles,
+ $path,
+ gutenberg_experimental_get( $context['styles'], $path )
+ );
+ }
+ }
+ }
+ }
+
+ // Settings escaping.
+ // For now the ony allowed settings are presets.
+ if ( ! empty( $context['settings'] ) ) {
+ foreach ( self::PRESETS_METADATA as $preset_metadata ) {
+ $current_preset = gutenberg_experimental_get( $context, $preset_metadata['path'], null );
+ if ( null !== $current_preset ) {
+ $escaped_preset = array();
+ foreach ( $current_preset as $single_preset ) {
+ if (
+ esc_attr( esc_html( $single_preset['name'] ) ) === $single_preset['name'] &&
+ sanitize_html_class( $single_preset['slug'] ) === $single_preset['slug']
+ ) {
+ $value = $single_preset[ $preset_metadata['value_key'] ];
+ $single_preset_is_valid = null;
+ if ( isset( $preset_metadata['classes'] ) && count( $preset_metadata['classes'] ) > 0 ) {
+ $single_preset_is_valid = true;
+ foreach ( $preset_metadata['classes'] as $class_meta_data ) {
+ $property = $class_meta_data['property_name'];
+ $style_to_validate = $property . ': ' . $value;
+ if ( esc_html( safecss_filter_attr( $style_to_validate ) ) !== $style_to_validate ) {
+ $single_preset_is_valid = false;
+ break;
+ }
+ }
+ } else {
+ $property = $preset_metadata['css_var_infix'];
+ $style_to_validate = $property . ': ' . $value;
+ $single_preset_is_valid = esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate;
+ }
+ if ( $single_preset_is_valid ) {
+ $escaped_preset[] = $single_preset;
+ }
+ }
+ }
+ if ( count( $escaped_preset ) > 0 ) {
+ if ( null === $escaped_settings ) {
+ $escaped_settings = array();
+ }
+ gutenberg_experimental_set( $escaped_settings, $preset_metadata['path'], $escaped_preset );
+ }
+ }
+ }
+ if ( null !== $escaped_settings ) {
+ $escaped_settings = $escaped_settings['settings'];
+ }
+ }
+
+ if ( null === $escaped_settings && null === $escaped_styles ) {
+ unset( $this->contexts[ $context_name ] );
+ } elseif ( null !== $escaped_settings && null !== $escaped_styles ) {
+ $context = array(
+ 'styles' => $escaped_styles,
+ 'settings' => $escaped_settings,
+ );
+ } elseif ( null === $escaped_settings ) {
+ $context = array(
+ 'styles' => $escaped_styles,
+ );
+ } else {
+ $context = array(
+ 'settings' => $escaped_settings,
+ );
+ }
+ }
+ }
+
/**
* Retuns the raw data.
*
diff --git a/lib/client-assets.php b/lib/client-assets.php
index 54c5f41d4d3a3..89be692a87b02 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -234,6 +234,14 @@ function gutenberg_register_vendor_scripts( $scripts ) {
'4.17.19',
true
);
+
+ gutenberg_register_vendor_script(
+ $scripts,
+ 'object-fit-polyfill',
+ 'https://unpkg.com/objectFitPolyfill@2.3.0/dist/objectFitPolyfill.min.js',
+ array(),
+ '2.3.0'
+ );
}
add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' );
@@ -303,7 +311,7 @@ function gutenberg_register_packages_styles( $styles ) {
$styles,
'wp-block-editor',
gutenberg_url( 'build/block-editor/style.css' ),
- array( 'wp-components', 'wp-editor-font' ),
+ array( 'wp-components' ),
filemtime( gutenberg_dir_path() . 'build/editor/style.css' )
);
$styles->add_data( 'wp-block-editor', 'rtl', 'replace' );
@@ -706,3 +714,29 @@ function gutenberg_extend_block_editor_styles_html() {
echo "";
}
add_action( 'admin_footer-toplevel_page_gutenberg-edit-site', 'gutenberg_extend_block_editor_styles_html' );
+
+/**
+ * Adds a polyfill for object-fit in environments which do not support it.
+ *
+ * The script registration occurs in `gutenberg_register_vendor_scripts`, which
+ * should be removed in coordination with this function.
+ *
+ * @see gutenberg_register_vendor_scripts
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit
+ *
+ * @since 9.1.0
+ *
+ * @param WP_Scripts $scripts WP_Scripts object.
+ */
+function gutenberg_add_object_fit_polyfill( $scripts ) {
+ did_action( 'init' ) && $scripts->add_inline_script(
+ 'wp-polyfill',
+ wp_get_script_polyfill(
+ $scripts,
+ array(
+ '"objectFit" in document.documentElement.style' => 'object-fit-polyfill',
+ )
+ )
+ );
+}
+add_action( 'wp_default_scripts', 'gutenberg_add_object_fit_polyfill', 20 );
diff --git a/lib/experimental-i18n-theme.json b/lib/experimental-i18n-theme.json
new file mode 100644
index 0000000000000..4731a59df0e97
--- /dev/null
+++ b/lib/experimental-i18n-theme.json
@@ -0,0 +1,32 @@
+{
+ "settings": {
+ "typography": {
+ "fontSizes": [
+ "name"
+ ],
+ "fontStyles": [
+ "name"
+ ],
+ "fontWeights": [
+ "name"
+ ],
+ "fontFamilies": [
+ "name"
+ ],
+ "textTransforms": [
+ "name"
+ ],
+ "textDecorations": [
+ "name"
+ ]
+ },
+ "color": {
+ "palette": [
+ "name"
+ ],
+ "gradients": [
+ "name"
+ ]
+ }
+ }
+}
diff --git a/lib/full-site-editing/block-templates.php b/lib/full-site-editing/block-templates.php
index bb5d26157ff35..a40a7e0187a07 100644
--- a/lib/full-site-editing/block-templates.php
+++ b/lib/full-site-editing/block-templates.php
@@ -105,6 +105,42 @@ function _gutenberg_get_template_files( $template_type ) {
return $template_files;
}
+/**
+ * Parses wp_template content and injects the current theme's
+ * stylesheet as a theme attribute into each wp_template_part
+ *
+ * @param string $template_content serialized wp_template content.
+ * @param string $theme the active theme's stylesheet.
+ *
+ * @return string Updated wp_template content.
+ */
+function _inject_theme_attribute_in_content( $template_content, $theme ) {
+ $has_updated_content = false;
+ $new_content = '';
+ $template_blocks = parse_blocks( $template_content );
+
+ foreach ( $template_blocks as $key => $block ) {
+ if (
+ 'core/template-part' === $block['blockName'] &&
+ ! isset( $block['attrs']['theme'] ) &&
+ wp_get_theme()->get_stylesheet() === $theme
+ ) {
+ $template_blocks[ $key ]['attrs']['theme'] = $theme;
+ $has_updated_content = true;
+ }
+ }
+
+ if ( $has_updated_content ) {
+ foreach ( $template_blocks as $block ) {
+ $new_content .= serialize_block( $block );
+ }
+
+ return $new_content;
+ } else {
+ return $template_content;
+ }
+}
+
/**
* Build a unified template object based on a theme file.
*
@@ -115,12 +151,17 @@ function _gutenberg_get_template_files( $template_type ) {
*/
function _gutenberg_build_template_result_from_file( $template_file, $template_type ) {
$default_template_types = gutenberg_get_default_template_types();
+ $template_content = file_get_contents( $template_file['path'] );
+ $theme = wp_get_theme()->get_stylesheet();
+
+ if ( 'wp_template' === $template_type ) {
+ $template_content = _inject_theme_attribute_in_content( $template_content, $theme );
+ }
- $theme = wp_get_theme()->get_stylesheet();
$template = new WP_Block_Template();
$template->id = $theme . '//' . $template_file['slug'];
$template->theme = $theme;
- $template->content = file_get_contents( $template_file['path'] );
+ $template->content = $template_content;
$template->slug = $template_file['slug'];
$template->is_custom = false;
$template->type = $template_type;
diff --git a/lib/full-site-editing/edit-site-export.php b/lib/full-site-editing/edit-site-export.php
index b299b51e16ee8..a08450f548b6c 100644
--- a/lib/full-site-editing/edit-site-export.php
+++ b/lib/full-site-editing/edit-site-export.php
@@ -5,6 +5,38 @@
* @package gutenberg
*/
+/**
+ * Parses wp_template content and injects the current theme's
+ * stylesheet as a theme attribute into each wp_template_part
+ *
+ * @param string $template_content serialized wp_template content.
+ *
+ * @return string Updated wp_template content.
+ */
+function _remove_theme_attribute_from_content( $template_content ) {
+ $has_updated_content = false;
+ $new_content = '';
+ $template_blocks = parse_blocks( $template_content );
+
+ foreach ( $template_blocks as $key => $block ) {
+ if ( 'core/template-part' === $block['blockName'] && isset( $block['attrs']['theme'] ) ) {
+ unset( $template_blocks[ $key ]['attrs']['theme'] );
+ $has_updated_content = true;
+ }
+ }
+
+ if ( $has_updated_content ) {
+ foreach ( $template_blocks as $block ) {
+ $new_content .= serialize_block( $block );
+ }
+
+ return $new_content;
+ } else {
+ return $template_content;
+ }
+}
+
+
/**
* Output a ZIP file with an export of the current templates
* and template parts from the site editor, and close the connection.
@@ -24,6 +56,9 @@ function gutenberg_edit_site_export() {
// Load templates into the zip file.
$templates = gutenberg_get_block_templates();
foreach ( $templates as $template ) {
+ $updated_content = _remove_theme_attribute_from_content( $template['content'] );
+ $template->content = $updated_content;
+
$zip->addFromString(
'theme/block-templates/' . $template->slug . '.html',
$template->content
diff --git a/lib/full-site-editing/edit-site-page.php b/lib/full-site-editing/edit-site-page.php
index d73ad831a5233..992e9b82a0982 100644
--- a/lib/full-site-editing/edit-site-page.php
+++ b/lib/full-site-editing/edit-site-page.php
@@ -50,7 +50,7 @@ function gutenberg_get_editor_styles() {
);
/* translators: Use this to specify the CSS font family for the default font. */
- $locale_font_family = esc_html_x( 'Noto Serif', 'CSS Font Family for Editor Font', 'gutenberg' );
+ $locale_font_family = '-apple-system, BlinkMacSystemFont,"Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell,"Helvetica Neue", sans-serif';
$styles[] = array(
'css' => "body { font-family: '$locale_font_family' }",
);
diff --git a/lib/global-styles.php b/lib/global-styles.php
index 800048bdcb219..5aab9277715e4 100644
--- a/lib/global-styles.php
+++ b/lib/global-styles.php
@@ -271,3 +271,112 @@ function gutenberg_experimental_global_styles_register_user_cpt() {
add_action( 'init', 'gutenberg_experimental_global_styles_register_user_cpt' );
add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings', PHP_INT_MAX );
add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' );
+
+
+/**
+ * Sanitizes global styles user content removing unsafe rules.
+ *
+ * @param string $content Post content to filter.
+ * @return string Filtered post content with unsafe rules removed.
+ */
+function gutenberg_global_styles_filter_post( $content ) {
+ $decoded_data = json_decode( stripslashes( $content ), true );
+ $json_decoding_error = json_last_error();
+ if (
+ JSON_ERROR_NONE === $json_decoding_error &&
+ is_array( $decoded_data ) &&
+ isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) &&
+ $decoded_data['isGlobalStylesUserThemeJSON']
+ ) {
+ unset( $decoded_data['isGlobalStylesUserThemeJSON'] );
+ $theme_json = new WP_Theme_JSON( $decoded_data );
+ $theme_json->remove_insecure_properties();
+ $data_to_encode = $theme_json->get_raw_data();
+ $data_to_encode['isGlobalStylesUserThemeJSON'] = true;
+ return wp_json_encode( $data_to_encode );
+ }
+ return $content;
+}
+
+/**
+ * Adds the filters to filter global styles user theme.json.
+ */
+function gutenberg_global_styles_kses_init_filters() {
+ add_filter( 'content_save_pre', 'gutenberg_global_styles_filter_post' );
+}
+
+/**
+ * Removes the filters to filter global styles user theme.json.
+ */
+function gutenberg_global_styles_kses_remove_filters() {
+ remove_filter( 'content_save_pre', 'gutenberg_global_styles_filter_post' );
+}
+
+/**
+ * Register global styles kses filters if the user does not have unfiltered_html capability.
+ *
+ * @uses render_block_core_navigation()
+ * @throws WP_Error An WP_Error exception parsing the block definition.
+ */
+function gutenberg_global_styles_kses_init() {
+ gutenberg_global_styles_kses_remove_filters();
+ if ( ! current_user_can( 'unfiltered_html' ) ) {
+ gutenberg_global_styles_kses_init_filters();
+ }
+}
+
+/**
+ * This filter is the last being executed on force_filtered_html_on_import.
+ * If the input of the filter is true it means we are in an import situation and should
+ * enable kses, independently of the user capabilities.
+ * So in that case we call gutenberg_global_styles_kses_init_filters;
+ *
+ * @param string $arg Input argument of the filter.
+ * @return string Exactly what was passed as argument.
+ */
+function gutenberg_global_styles_force_filtered_html_on_import_filter( $arg ) {
+ // force_filtered_html_on_import is true we need to init the global styles kses filters.
+ if ( $arg ) {
+ gutenberg_global_styles_kses_init_filters();
+ }
+ return $arg;
+}
+
+/**
+ * This filter is the last being executed on force_filtered_html_on_import.
+ * If the input of the filter is true it means we are in an import situation and should
+ * enable kses, independently of the user capabilities.
+ * So in that case we call gutenberg_global_styles_kses_init_filters;
+ *
+ * @param bool $allow_css Whether the CSS in the test string is considered safe.
+ * @param bool $css_test_string The CSS string to test..
+ * @return bool If $allow_css is true it returns true.
+ * If $allow_css is false and the CSS rule is referencing a WordPress css variable it returns true.
+ * Otherwise the function return false.
+ */
+function gutenberg_global_styles_include_support_for_wp_variables( $allow_css, $css_test_string ) {
+ if ( $allow_css ) {
+ return $allow_css;
+ }
+ $allowed_preset_attributes = array(
+ 'background',
+ 'background-color',
+ 'color',
+ 'font-family',
+ 'font-size',
+ );
+ $parts = explode( ':', $css_test_string, 2 );
+
+ if ( ! in_array( trim( $parts[0] ), $allowed_preset_attributes, true ) ) {
+ return $allow_css;
+ }
+ return ! ! preg_match( '/^var\(--wp-[a-zA-Z0-9\-]+\)$/', trim( $parts[1] ) );
+}
+
+
+add_action( 'init', 'gutenberg_global_styles_kses_init' );
+add_action( 'set_current_user', 'gutenberg_global_styles_kses_init' );
+add_filter( 'force_filtered_html_on_import', 'gutenberg_global_styles_force_filtered_html_on_import_filter', 999 );
+add_filter( 'safecss_filter_attr_allow_css', 'gutenberg_global_styles_include_support_for_wp_variables', 10, 2 );
+// This filter needs to be executed last.
+
diff --git a/lib/upgrade.php b/lib/upgrade.php
index fe80cc0b7e89e..2573ed6d575da 100644
--- a/lib/upgrade.php
+++ b/lib/upgrade.php
@@ -53,6 +53,10 @@ function _gutenberg_migrate_remove_fse_drafts() {
if ( $term ) {
wp_delete_term( $term->term_id, 'wp-theme' );
}
+
+ // Delete useless options.
+ delete_option( 'gutenberg_last_synchronize_theme_template_checks' );
+ delete_option( 'gutenberg_last_synchronize_theme_template-part_checks' );
}
add_action( 'plugins_loaded', '_gutenberg_migrate_database' );
diff --git a/package-lock.json b/package-lock.json
index cf92071bf6142..c9150a55bd081 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "gutenberg",
- "version": "9.7.0",
+ "version": "9.8.0-rc.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -11694,7 +11694,6 @@
"react-autosize-textarea": "^7.1.0",
"react-merge-refs": "^1.0.0",
"react-spring": "^8.0.19",
- "reakit": "1.3.4",
"redux-multi": "^0.1.12",
"rememo": "^3.0.0",
"tinycolor2": "^1.4.1",
@@ -11739,7 +11738,6 @@
"memize": "^1.1.0",
"moment": "^2.22.1",
"react-easy-crop": "^3.0.0",
- "reakit": "1.3.4",
"tinycolor2": "^1.4.1"
}
},
@@ -11885,6 +11883,10 @@
"write-pkg": "^4.0.0"
}
},
+ "@wordpress/create-block-tutorial-template": {
+ "version": "file:packages/create-block-tutorial-template",
+ "dev": true
+ },
"@wordpress/custom-templated-path-webpack-plugin": {
"version": "file:packages/custom-templated-path-webpack-plugin",
"dev": true,
@@ -12163,7 +12165,6 @@
"lodash": "^4.17.19",
"memize": "^1.1.0",
"react-autosize-textarea": "^7.1.0",
- "refx": "^3.0.0",
"rememo": "^3.0.0"
}
},
@@ -47867,11 +47868,6 @@
}
}
},
- "refx": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/refx/-/refx-3.0.0.tgz",
- "integrity": "sha512-qmd73YvYiVWfKPECtE90ujmPwwtAnmtEOkBKgfNEuqJ4trTeKbqFV2UY878yFvHBvU7BBu4/w/Q8pk/t0zDpYA=="
- },
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
diff --git a/package.json b/package.json
index 35a9a07afef03..3d15cf55539cd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "gutenberg",
- "version": "9.7.0",
+ "version": "9.8.0-rc.1",
"private": true,
"description": "A new WordPress editor experience.",
"author": "The WordPress Contributors",
@@ -115,6 +115,7 @@
"@wordpress/base-styles": "file:packages/base-styles",
"@wordpress/browserslist-config": "file:packages/browserslist-config",
"@wordpress/create-block": "file:packages/create-block",
+ "@wordpress/create-block-tutorial-template": "file:packages/create-block-tutorial-template",
"@wordpress/custom-templated-path-webpack-plugin": "file:packages/custom-templated-path-webpack-plugin",
"@wordpress/dependency-extraction-webpack-plugin": "file:packages/dependency-extraction-webpack-plugin",
"@wordpress/docgen": "file:packages/docgen",
diff --git a/packages/annotations/src/block/index.js b/packages/annotations/src/block/index.js
index 3a5dee89607ab..8342eb8e32609 100644
--- a/packages/annotations/src/block/index.js
+++ b/packages/annotations/src/block/index.js
@@ -4,6 +4,10 @@
import { addFilter } from '@wordpress/hooks';
import { withSelect } from '@wordpress/data';
+/**
+ * Internal dependencies
+ */
+import { STORE_NAME } from '../store/constants';
/**
* Adds annotation className to the block-list-block component.
*
@@ -13,7 +17,7 @@ import { withSelect } from '@wordpress/data';
const addAnnotationClassName = ( OriginalComponent ) => {
return withSelect( ( select, { clientId, className } ) => {
const annotations = select(
- 'core/annotations'
+ STORE_NAME
).__experimentalGetAnnotationsForBlock( clientId );
return {
diff --git a/packages/annotations/src/format/annotation.js b/packages/annotations/src/format/annotation.js
index f3ea7096a3782..dd0d618f9c678 100644
--- a/packages/annotations/src/format/annotation.js
+++ b/packages/annotations/src/format/annotation.js
@@ -7,7 +7,10 @@ import { applyFormat, removeFormat } from '@wordpress/rich-text';
const FORMAT_NAME = 'core/annotation';
const ANNOTATION_ATTRIBUTE_PREFIX = 'annotation-text-';
-const STORE_NAME = 'core/annotations';
+/**
+ * Internal dependencies
+ */
+import { STORE_NAME } from '../store/constants';
/**
* Applies given annotations to the given record.
diff --git a/packages/annotations/src/store/constants.js b/packages/annotations/src/store/constants.js
new file mode 100644
index 0000000000000..4b8cf0416eb92
--- /dev/null
+++ b/packages/annotations/src/store/constants.js
@@ -0,0 +1,6 @@
+/**
+ * The identifier for the data store.
+ *
+ * @type {string}
+ */
+export const STORE_NAME = 'core/annotations';
diff --git a/packages/annotations/src/store/index.js b/packages/annotations/src/store/index.js
index 81dee6efd82b4..50c3cab06a72e 100644
--- a/packages/annotations/src/store/index.js
+++ b/packages/annotations/src/store/index.js
@@ -13,7 +13,7 @@ import * as actions from './actions';
/**
* Module Constants
*/
-const STORE_NAME = 'core/annotations';
+import { STORE_NAME } from './constants';
/**
* Store definition for the annotations namespace.
diff --git a/packages/base-styles/_variables.scss b/packages/base-styles/_variables.scss
index c51a12db7b68c..e0c2a409028bd 100644
--- a/packages/base-styles/_variables.scss
+++ b/packages/base-styles/_variables.scss
@@ -14,7 +14,6 @@
$default-font: -apple-system, BlinkMacSystemFont,"Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell,"Helvetica Neue", sans-serif;
$default-font-size: 13px;
$default-line-height: 1.4;
-$editor-font: "Noto Serif", serif;
$editor-html-font: Menlo, Consolas, monaco, monospace;
$editor-font-size: 16px;
$default-block-margin: 28px; // This value provides a consistent, contiguous spacing between blocks.
diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss
index 14c40bdeebd86..630323a871348 100644
--- a/packages/base-styles/_z-index.scss
+++ b/packages/base-styles/_z-index.scss
@@ -32,6 +32,7 @@ $z-layers: (
".block-library-button__inline-link .block-editor-url-input__suggestions": 6, // URL suggestions for button block above sibling inserter
".wp-block-cover__inner-container": 1, // InnerBlocks area inside cover image block
".wp-block-cover.has-background-dim::before": 1, // Overlay area inside block cover need to be higher than the video background.
+ ".wp-block-cover__image-background": 0, // Image background inside cover block.
".wp-block-cover__video-background": 0, // Video background inside cover block.
".wp-block-template-part__placeholder-preview-filter-input": 1,
diff --git a/packages/block-directory/src/components/downloadable-blocks-panel/index.js b/packages/block-directory/src/components/downloadable-blocks-panel/index.js
index fa6af8b174dbd..2929e5dc439a5 100644
--- a/packages/block-directory/src/components/downloadable-blocks-panel/index.js
+++ b/packages/block-directory/src/components/downloadable-blocks-panel/index.js
@@ -7,6 +7,7 @@ import { withSelect } from '@wordpress/data';
import { __, _n, sprintf } from '@wordpress/i18n';
import { Spinner } from '@wordpress/components';
import { speak } from '@wordpress/a11y';
+import { store as blockEditorStore } from '@wordpress/block-editor';
/**
* Internal dependencies
@@ -77,18 +78,26 @@ function DownloadableBlocksPanel( {
}
export default compose( [
- withSelect( ( select, { filterValue } ) => {
+ withSelect( ( select, { filterValue, rootClientId = null } ) => {
const {
getDownloadableBlocks,
isRequestingDownloadableBlocks,
} = select( blockDirectoryStore );
+ const { canInsertBlockType } = select( blockEditorStore );
const hasPermission = select( 'core' ).canUser(
'read',
'block-directory/search'
);
+
+ function getInstallableBlocks( term ) {
+ return getDownloadableBlocks( term ).filter( ( block ) =>
+ canInsertBlockType( block, rootClientId, true )
+ );
+ }
+
const downloadableItems = hasPermission
- ? getDownloadableBlocks( filterValue )
+ ? getInstallableBlocks( filterValue )
: [];
const isLoading = isRequestingDownloadableBlocks( filterValue );
diff --git a/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js b/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js
index b91a3692b6d29..fa96d684791c4 100644
--- a/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js
+++ b/packages/block-directory/src/plugins/inserter-menu-downloadable-blocks-panel/index.js
@@ -21,7 +21,13 @@ function InserterMenuDownloadableBlocksPanel() {
return (
<__experimentalInserterMenuExtension>
- { ( { onSelect, onHover, filterValue, hasItems } ) => {
+ { ( {
+ onSelect,
+ onHover,
+ filterValue,
+ hasItems,
+ rootClientId,
+ } ) => {
if ( hasItems || ! filterValue ) {
return null;
}
@@ -34,6 +40,7 @@ function InserterMenuDownloadableBlocksPanel() {
diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json
index 4df1e348d459d..05841cb049ec5 100644
--- a/packages/block-editor/package.json
+++ b/packages/block-editor/package.json
@@ -62,7 +62,6 @@
"react-autosize-textarea": "^7.1.0",
"react-merge-refs": "^1.0.0",
"react-spring": "^8.0.19",
- "reakit": "1.3.4",
"redux-multi": "^0.1.12",
"rememo": "^3.0.0",
"tinycolor2": "^1.4.1",
diff --git a/packages/block-editor/src/components/alignment-toolbar/index.js b/packages/block-editor/src/components/alignment-toolbar/index.js
index 8eed48b364e48..18e9f4002b681 100644
--- a/packages/block-editor/src/components/alignment-toolbar/index.js
+++ b/packages/block-editor/src/components/alignment-toolbar/index.js
@@ -38,7 +38,8 @@ export function AlignmentToolbar( props ) {
value,
onChange,
alignmentControls = DEFAULT_ALIGNMENT_CONTROLS,
- label = __( 'Change text alignment' ),
+ label = __( 'Align' ),
+ describedBy = __( 'Change text alignment' ),
isCollapsed = true,
} = props;
@@ -61,6 +62,7 @@ export function AlignmentToolbar( props ) {
isCollapsed={ isCollapsed }
icon={ setIcon() }
label={ label }
+ toggleProps={ { describedBy } }
popoverProps={ POPOVER_PROPS }
controls={ alignmentControls.map( ( control ) => {
const { align } = control;
diff --git a/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap
index f995c767f9542..c6699db92e496 100644
--- a/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap
+++ b/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap
@@ -47,13 +47,18 @@ exports[`AlignmentToolbar should allow custom alignment controls to be specified
}
isCollapsed={true}
- label="Change text alignment"
+ label="Align"
popoverProps={
Object {
"isAlternate": true,
"position": "bottom right",
}
}
+ toggleProps={
+ Object {
+ "describedBy": "Change text alignment",
+ }
+ }
/>
`;
@@ -119,12 +124,17 @@ exports[`AlignmentToolbar should match snapshot 1`] = `
}
isCollapsed={true}
- label="Change text alignment"
+ label="Align"
popoverProps={
Object {
"isAlternate": true,
"position": "bottom right",
}
}
+ toggleProps={
+ Object {
+ "describedBy": "Change text alignment",
+ }
+ }
/>
`;
diff --git a/packages/block-editor/src/components/block-alignment-toolbar/index.js b/packages/block-editor/src/components/block-alignment-toolbar/index.js
index 769e96d454219..e14edfac6f47e 100644
--- a/packages/block-editor/src/components/block-alignment-toolbar/index.js
+++ b/packages/block-editor/src/components/block-alignment-toolbar/index.js
@@ -96,7 +96,8 @@ export function BlockAlignmentToolbar( {
? activeAlignmentControl.icon
: defaultAlignmentControl.icon
}
- label={ __( 'Change alignment' ) }
+ label={ __( 'Align' ) }
+ toggleProps={ { describedBy: __( 'Change alignment' ) } }
controls={ enabledControls.map( ( control ) => {
return {
...BLOCK_ALIGNMENTS_CONTROLS[ control ],
diff --git a/packages/block-editor/src/components/block-alignment-toolbar/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/block-alignment-toolbar/test/__snapshots__/index.js.snap
index 564a49e4dd238..5910fc1b29e97 100644
--- a/packages/block-editor/src/components/block-alignment-toolbar/test/__snapshots__/index.js.snap
+++ b/packages/block-editor/src/components/block-alignment-toolbar/test/__snapshots__/index.js.snap
@@ -59,11 +59,16 @@ exports[`BlockAlignmentToolbar should match snapshot 1`] = `
}
isCollapsed={true}
- label="Change alignment"
+ label="Align"
popoverProps={
Object {
"isAlternate": true,
}
}
+ toggleProps={
+ Object {
+ "describedBy": "Change alignment",
+ }
+ }
/>
`;
diff --git a/packages/block-editor/src/components/block-card/README.md b/packages/block-editor/src/components/block-card/README.md
index f34ef2b23c0d9..cc8740580952e 100644
--- a/packages/block-editor/src/components/block-card/README.md
+++ b/packages/block-editor/src/components/block-card/README.md
@@ -6,13 +6,11 @@ In the editor, this component is displayed in two different places: in the block
![Heading block card in the block inspector](https://make.wordpress.org/core/files/2020/09/screenshot-wordpress.org-2020.09.08-14_19_21.png)
-
## Table of contents
1. [Development guidelines](#development-guidelines)
2. [Related components](#related-components)
-
## Development guidelines
### Usage
@@ -21,20 +19,37 @@ Renders a block card with default style.
```jsx
import { BlockCard } from '@wordpress/block-editor';
+import { paragraph } from '@wordpress/icons';
+
+const MyBlockCard = () => (
+
+);
+```
-const MyBlockCard = () => ;
+### Props
-```
+#### icon
+
+- **Type:** `String` | `Object`
+
+The icon of the block. This can be any of [WordPress' Dashicons](https://developer.wordpress.org/resource/dashicons/), or a custom `svg` element.
+
+#### title
-### props
+- **Type:** `String`
-#### blockType
+The title of the block.
-The type of the block that is being displayed in
+#### description
-Type: `Object`
+- **Type:** `String`
+The description of the block.
## Related components
-Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/provider/README.md) in the components tree.
+Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/provider/README.md) in the components tree.
diff --git a/packages/block-editor/src/components/block-list-appender/style.scss b/packages/block-editor/src/components/block-list-appender/style.scss
index 1b893c26875d7..db86e8e8da012 100644
--- a/packages/block-editor/src/components/block-list-appender/style.scss
+++ b/packages/block-editor/src/components/block-list-appender/style.scss
@@ -1,7 +1,11 @@
// These styles are only applied to the appender when it appears inside of a block.
// Otherwise the default appender may be improperly positioned in some themes.
.block-editor-block-list__block .block-list-appender {
- margin: $grid-unit-10 0;
+ margin: 0;
+
+ .block-editor-default-block-appender {
+ margin: $grid-unit-10 0;
+ }
// Animate appearance.
.block-list-appender__toggle {
diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss
index ef582793f9b9f..23acd106a185a 100644
--- a/packages/block-editor/src/components/block-list/style.scss
+++ b/packages/block-editor/src/components/block-list/style.scss
@@ -236,6 +236,21 @@
}
}
+ // Active entity spotlight.
+ &.has-active-entity:not(.is-focus-mode) {
+ opacity: 0.5;
+ transition: opacity 0.1s linear;
+ @include reduce-motion("transition");
+
+ &.is-active-entity,
+ &.has-child-selected,
+ &:not(.has-child-selected) .block-editor-block-list__block,
+ &.is-active-entity .block-editor-block-list__block,
+ .is-active-entity .block-editor-block-list__block {
+ opacity: 1;
+ }
+ }
+
/**
* Block styles and alignments
*/
diff --git a/packages/block-editor/src/components/block-mover/index.js b/packages/block-editor/src/components/block-mover/index.js
index 7ba38ce5f039a..d727e56b65eb5 100644
--- a/packages/block-editor/src/components/block-mover/index.js
+++ b/packages/block-editor/src/components/block-mover/index.js
@@ -40,8 +40,7 @@ function BlockMover( {
return null;
}
- const dragHandleLabel =
- clientIds.length === 1 ? __( 'Drag block' ) : __( 'Drag blocks' );
+ const dragHandleLabel = __( 'Drag' );
// We emulate a disabled state because forcefully applying the `disabled`
// attribute on the buttons while it has focus causes the screen to change
diff --git a/packages/block-editor/src/components/block-navigation/appender.js b/packages/block-editor/src/components/block-navigation/appender.js
index 58bdb381e10e6..5ea05906062c0 100644
--- a/packages/block-editor/src/components/block-navigation/appender.js
+++ b/packages/block-editor/src/components/block-navigation/appender.js
@@ -15,7 +15,7 @@ import { useSelect } from '@wordpress/data';
* Internal dependencies
*/
import BlockNavigationLeaf from './leaf';
-import DescenderLines from './descender-lines';
+import Indentation from './indentation';
import Inserter from '../inserter';
export default function BlockNavigationAppender( {
@@ -23,7 +23,6 @@ export default function BlockNavigationAppender( {
position,
level,
rowCount,
- terminatedLevels,
path,
} ) {
const isDragging = useSelect(
@@ -63,11 +62,7 @@ export default function BlockNavigationAppender( {
>
{ ( { ref, tabIndex, onFocus } ) => (
-
+
+
{ blockDisplayName }
{ isSelected && (
diff --git a/packages/block-editor/src/components/block-navigation/block.js b/packages/block-editor/src/components/block-navigation/block.js
index da24893d49d0d..6ae8a43a376ed 100644
--- a/packages/block-editor/src/components/block-navigation/block.js
+++ b/packages/block-editor/src/components/block-navigation/block.js
@@ -25,7 +25,6 @@ import {
BlockMoverUpButton,
BlockMoverDownButton,
} from '../block-mover/button';
-import DescenderLines from './descender-lines';
import BlockNavigationBlockContents from './block-contents';
import BlockSettingsDropdown from '../block-settings-menu/block-settings-dropdown';
import { useBlockNavigationContext } from './context';
@@ -39,7 +38,6 @@ export default function BlockNavigationBlock( {
rowCount,
siblingBlockCount,
showBlockMovers,
- terminatedLevels,
path,
} ) {
const cellRef = useRef( null );
@@ -108,11 +106,6 @@ export default function BlockNavigationBlock( {
>
{ ( { ref, tabIndex, onFocus } ) => (
-
onClick( block.clientId ) }
diff --git a/packages/block-editor/src/components/block-navigation/descender-lines.js b/packages/block-editor/src/components/block-navigation/indentation.js
similarity index 58%
rename from packages/block-editor/src/components/block-navigation/descender-lines.js
rename to packages/block-editor/src/components/block-navigation/indentation.js
index 58b7144bd6dd8..6ec4e5206134c 100644
--- a/packages/block-editor/src/components/block-navigation/descender-lines.js
+++ b/packages/block-editor/src/components/block-navigation/indentation.js
@@ -4,15 +4,11 @@
import { times } from 'lodash';
import classnames from 'classnames';
-const lineClassName = 'block-editor-block-navigator-descender-line';
+const lineClassName = 'block-editor-block-navigator-indentation';
-export default function DescenderLines( {
- level,
- isLastRow,
- terminatedLevels,
-} ) {
+export default function Indentation( { level } ) {
return times( level - 1, ( index ) => {
- // The first 'level' that has a descender line is level 2.
+ // The first 'level' that has an indentation is level 2.
// Add 2 to the zero-based index below to reflect that.
const currentLevel = index + 2;
const hasItem = currentLevel === level;
@@ -22,8 +18,6 @@ export default function DescenderLines( {
aria-hidden="true"
className={ classnames( lineClassName, {
'has-item': hasItem,
- 'is-last-row': isLastRow,
- 'is-terminated': terminatedLevels.includes( currentLevel ),
} ) }
/>
);
diff --git a/packages/block-editor/src/components/block-navigation/index.js b/packages/block-editor/src/components/block-navigation/index.js
index 4e18231de1d66..ccce62a2c3313 100644
--- a/packages/block-editor/src/components/block-navigation/index.js
+++ b/packages/block-editor/src/components/block-navigation/index.js
@@ -36,23 +36,14 @@ function BlockNavigation( {
{ __( 'List view' ) }
- { hasHierarchy && (
-
- ) }
- { ! hasHierarchy && (
-
- ) }
+
+
);
}
diff --git a/packages/block-editor/src/components/block-navigation/style.scss b/packages/block-editor/src/components/block-navigation/style.scss
index 9d5dfb714a4a3..546dab181208e 100644
--- a/packages/block-editor/src/components/block-navigation/style.scss
+++ b/packages/block-editor/src/components/block-navigation/style.scss
@@ -1,6 +1,3 @@
-$tree-border-width: 2px;
-$tree-item-height: 36px;
-
.block-editor-block-navigation__label {
margin: 0 0 $grid-unit-15;
color: $gray-700;
@@ -10,7 +7,7 @@ $tree-item-height: 36px;
}
.block-editor-block-navigation__container {
- padding: $grid-unit - $border-width;
+ min-width: 280px;
}
.block-editor-block-navigation-tree {
@@ -24,6 +21,13 @@ $tree-item-height: 36px;
// Use position relative for row animation.
position: relative;
+ &.is-selected .block-editor-block-navigation-block-contents,
+ &.is-selected:hover .block-editor-block-navigation-block-contents,
+ &.is-selected:focus .block-editor-block-navigation-block-contents {
+ background: $gray-900;
+ color: $white;
+ }
+
&.is-dragging {
display: none;
}
@@ -33,15 +37,21 @@ $tree-item-height: 36px;
align-items: center;
width: 100%;
height: auto;
- padding: $grid-unit-15 ($grid-unit-15 / 2);
- margin-top: auto;
- margin-bottom: auto;
+ padding: ($grid-unit-15 / 2);
text-align: left;
color: $gray-900;
border-radius: 2px;
position: relative;
white-space: nowrap;
+ &:hover {
+ background: $gray-100;
+ }
+
+ &:focus {
+ z-index: 1;
+ }
+
&.is-dropping-before::before {
content: "";
position: absolute;
@@ -91,14 +101,6 @@ $tree-item-height: 36px;
margin-right: 6px;
}
- &.is-selected .block-editor-block-icon svg,
- &.is-selected:focus .block-editor-block-icon svg {
- color: $white;
- background: $gray-900;
- box-shadow: 0 0 0 $border-width $gray-900;
- border-radius: $border-width;
- }
-
.block-editor-block-navigation-block__menu-cell,
.block-editor-block-navigation-block__mover-cell,
.block-editor-block-navigation-block__contents-cell {
@@ -225,64 +227,9 @@ $tree-item-height: 36px;
.block-editor-block-navigation-appender__container {
display: flex;
}
-
- .block-editor-block-navigation-block__contents-container {
- min-height: $grid-unit-60;
- }
-
- .block-editor-block-navigator-descender-line {
- position: relative;
- flex-shrink: 0;
- width: ( $button-size / 2 ) + 6px;
-
- &:first-child {
- width: ( $button-size / 2 );
- }
-
- &.has-item {
- margin-right: 6px;
- }
-
- // Draw a vertical line using border-right.
- &::before {
- content: "";
- display: block;
- position: absolute;
- top: 1px;
- bottom: -2px;
- right: -1px;
- border-right: 2px solid $gray-600;
- }
-
- // If a vertical line has terminated, don't draw it.
- &.is-terminated::before {
- border-color: transparent;
- }
-
- // Make the last vertical line half-height.
- &.has-item.is-last-row {
- height: 26px;
- }
-
- // Draw a horizontal line using border-bottom.
- &.has-item::after {
- content: "";
- display: block;
- position: absolute;
- top: 26px;
- left: 100%;
- width: 5px;
- border-bottom: 2px solid $gray-600;
- }
- }
}
-// Position the horizontal line in the middle of appender's height
-.block-editor-block-navigation-appender__cell .block-editor-block-navigator-descender-line {
- &.has-item.is-last-row {
- height: $grid-unit-20;
- &::after {
- top: 100%;
- }
- }
+.block-editor-block-navigator-indentation {
+ flex-shrink: 0;
+ width: 18px;
}
diff --git a/packages/block-editor/src/components/block-settings-menu-controls/index.js b/packages/block-editor/src/components/block-settings-menu-controls/index.js
index 2509c794c978a..e24d904d1e280 100644
--- a/packages/block-editor/src/components/block-settings-menu-controls/index.js
+++ b/packages/block-editor/src/components/block-settings-menu-controls/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { compact, isEmpty, map } from 'lodash';
+import { compact, map } from 'lodash';
/**
* WordPress dependencies
@@ -9,6 +9,11 @@ import { compact, isEmpty, map } from 'lodash';
import { createSlotFill, MenuGroup } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
+/**
+ * Internal dependencies
+ */
+import ConvertToGroupButton from '../convert-to-group-buttons';
+
const { Fill: BlockSettingsMenuControls, Slot } = createSlotFill(
'BlockSettingsMenuControls'
);
@@ -31,9 +36,14 @@ const BlockSettingsMenuControlsSlot = ( { fillProps, clientIds = null } ) => {
return (
- { ( fills ) =>
- ! isEmpty( fills ) && { fills }
- }
+ { ( fills ) => {
+ return (
+
+ { fills }
+
+
+ );
+ } }
);
};
diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js
index acb7bfcf39d31..7a9c54f4cd234 100644
--- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js
+++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js
@@ -95,7 +95,7 @@ export function BlockSettingsDropdown( {
} ) => (
{
const { replaceBlocks } = useDispatch( blockEditorStore );
const blockInformation = useBlockDisplayInformation( blocks[ 0 ].clientId );
- const { possibleBlockTransformations, hasBlockStyles, icon } = useSelect(
+ const {
+ possibleBlockTransformations,
+ hasBlockStyles,
+ icon,
+ blockTitle,
+ } = useSelect(
( select ) => {
const { getBlockRootClientId, getBlockTransformItems } = select(
blockEditorStore
@@ -61,10 +66,12 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
),
hasBlockStyles: !! styles?.length,
icon: _icon,
+ blockTitle: getBlockType( firstBlockName ).title,
};
},
[ clientIds, blocks, blockInformation?.icon ]
);
+
const onTransform = ( name ) =>
replaceBlocks( clientIds, switchToBlockType( blocks, name ) );
const hasPossibleBlockTransformations = !! possibleBlockTransformations.length;
@@ -74,13 +81,16 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
}
/>
);
}
- const blockSwitcherLabel =
+
+ const blockSwitcherLabel = blockTitle;
+
+ const blockSwitcherDescription =
1 === blocks.length
? __( 'Change block type or style' )
: sprintf(
@@ -112,7 +122,10 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => {
showColors
/>
}
- toggleProps={ toggleProps }
+ toggleProps={ {
+ describedBy: blockSwitcherDescription,
+ ...toggleProps,
+ } }
menuProps={ { orientation: 'both' } }
>
{ ( { onClose } ) =>
@@ -155,9 +168,13 @@ export const BlockSwitcher = ( { clientIds } ) => {
[ clientIds ]
);
- return !! blocks?.length ? (
+ if ( ! blocks.length || blocks.some( ( block ) => ! block ) ) {
+ return null;
+ }
+
+ return (
- ) : null;
+ );
};
export default BlockSwitcher;
diff --git a/packages/block-editor/src/components/block-switcher/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/block-switcher/test/__snapshots__/index.js.snap
index a8ec2981e622b..b66f639d65af9 100644
--- a/packages/block-editor/src/components/block-switcher/test/__snapshots__/index.js.snap
+++ b/packages/block-editor/src/components/block-switcher/test/__snapshots__/index.js.snap
@@ -20,7 +20,6 @@ exports[`BlockSwitcherDropdownMenu should render disabled block switcher with mu
showColors={true}
/>
}
- title="Block icon"
/>
`;
diff --git a/packages/block-editor/src/components/block-switcher/test/index.js b/packages/block-editor/src/components/block-switcher/test/index.js
index b3048a3750586..6fda7e21f2ff0 100644
--- a/packages/block-editor/src/components/block-switcher/test/index.js
+++ b/packages/block-editor/src/components/block-switcher/test/index.js
@@ -25,6 +25,16 @@ describe( 'BlockSwitcher', () => {
const wrapper = shallow( );
expect( wrapper.html() ).toBeNull();
} );
+
+ test( 'should not render block switcher with null blocks', () => {
+ useSelect.mockImplementation( () => ( { blocks: [ null ] } ) );
+ const wrapper = shallow(
+
+ );
+ expect( wrapper.html() ).toBeNull();
+ } );
} );
describe( 'BlockSwitcherDropdownMenu', () => {
const headingBlock1 = {
diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss
index c9499b4731dfd..432a7374f612b 100644
--- a/packages/block-editor/src/components/block-toolbar/style.scss
+++ b/packages/block-editor/src/components/block-toolbar/style.scss
@@ -106,3 +106,100 @@
transform: translateY(-($block-toolbar-height + $grid-unit-15));
}
}
+
+.show-icon-labels {
+ .block-editor-block-toolbar {
+ .components-button.has-icon {
+ width: auto;
+
+ // Hide the button icons when labels are set to display...
+ svg {
+ display: none;
+ }
+ // ... and display labels.
+ &::after {
+ content: attr(aria-label);
+ font-size: $helptext-font-size;
+ }
+ }
+ }
+
+ // Padding overrides.
+
+ .components-accessible-toolbar .components-toolbar-group > div:first-child:last-child > .components-button.has-icon {
+ padding-left: 6px;
+ padding-right: 6px;
+ }
+
+ // Switcher overrides.
+ .block-editor-block-switcher {
+ border-right: 1px solid $gray-900;
+
+ .components-dropdown-menu__toggle {
+ margin-left: 0;
+ }
+ }
+
+ .block-editor-block-switcher .components-dropdown-menu__toggle,
+ .block-editor-block-switcher__no-switcher-icon {
+ .block-editor-block-icon {
+ width: 0 !important;
+ height: 0 !important;
+ }
+
+ &:focus::before {
+ right: $grid-unit-05 !important;
+ }
+ }
+
+ // Parent selector overrides
+
+ .block-editor-block-parent-selector__button {
+ .block-editor-block-icon {
+ width: 0;
+ }
+ }
+
+ // Mover overrides.
+ .block-editor-block-toolbar__block-controls .block-editor-block-mover {
+ margin-left: 0;
+ white-space: nowrap;
+ }
+
+ .block-editor-block-mover-button {
+ // The specificity can be reduced once https://github.com/WordPress/gutenberg/blob/try/block-toolbar-labels/packages/block-editor/src/components/block-mover/style.scss#L34 is also dealt with.
+ padding-left: $grid-unit !important;
+ padding-right: $grid-unit !important;
+ }
+
+ .block-editor-block-mover__drag-handle.has-icon {
+ padding-left: 6px !important;
+ padding-right: 6px !important;
+ border-right: 1px solid $gray-900;
+ }
+
+ @include break-small() {
+ // Specificity override for https://github.com/WordPress/gutenberg/blob/try/block-toolbar-labels/packages/block-editor/src/components/block-mover/style.scss#L69
+ .is-up-button.is-up-button.is-up-button {
+ border-bottom: 1px solid $gray-900;
+ margin-right: 0;
+ border-radius: 0;
+ }
+ }
+
+ .block-editor-block-contextual-toolbar .block-editor-block-mover.is-horizontal .block-editor-block-mover-button.block-editor-block-mover-button {
+ width: auto;
+ }
+
+ // Mobile adjustments
+ .components-toolbar,
+ .components-toolbar-group {
+ flex-shrink: 1;
+ }
+
+ .block-editor-format-toolbar {
+ .components-button + .components-button {
+ margin-left: 6px;
+ }
+ }
+}
diff --git a/packages/block-editor/src/components/block-types-list/index.js b/packages/block-editor/src/components/block-types-list/index.js
index a569a8c13d4bd..ca54df0d24b78 100644
--- a/packages/block-editor/src/components/block-types-list/index.js
+++ b/packages/block-editor/src/components/block-types-list/index.js
@@ -1,12 +1,11 @@
-/**
- * External dependencies
- */
-import { Composite, useCompositeState } from 'reakit';
-
/**
* WordPress dependencies
*/
import { getBlockMenuDefaultClassName } from '@wordpress/blocks';
+import {
+ __unstableComposite as Composite,
+ __unstableUseCompositeState as useCompositeState,
+} from '@wordpress/components';
/**
* Internal dependencies
diff --git a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap
index 789949e13e9c0..1328665a03eb5 100644
--- a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap
+++ b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap
@@ -51,6 +51,7 @@ exports[`ColorPaletteControl matches the snapshot 1`] = `
className="components-circular-option-picker__option-wrapper"
>
{} } ) {
+ const { replaceBlocks } = useDispatch( blockEditorStore );
+ const {
+ clientIds,
+ isGroupable,
+ isUngroupable,
+ blocksSelection,
+ groupingBlockName,
+ } = useSelect( ( select ) => {
+ const {
+ getBlockRootClientId,
+ getBlocksByClientId,
+ canInsertBlockType,
+ getSelectedBlockClientIds,
+ } = select( blockEditorStore );
+ const { getGroupingBlockName } = select( blocksStore );
+
+ const _clientIds = getSelectedBlockClientIds();
+ const _groupingBlockName = getGroupingBlockName();
+
+ const rootClientId = !! _clientIds?.length
+ ? getBlockRootClientId( _clientIds[ 0 ] )
+ : undefined;
+
+ const groupingBlockAvailable = canInsertBlockType(
+ _groupingBlockName,
+ rootClientId
+ );
+
+ const _blocksSelection = getBlocksByClientId( _clientIds );
+
+ const isSingleGroupingBlock =
+ _blocksSelection.length === 1 &&
+ _blocksSelection[ 0 ]?.name === _groupingBlockName;
+
+ // Do we have
+ // 1. Grouping block available to be inserted?
+ // 2. One or more blocks selected
+ // (we allow single Blocks to become groups unless
+ // they are a soltiary group block themselves)
+ const _isGroupable =
+ groupingBlockAvailable &&
+ _blocksSelection.length &&
+ ! isSingleGroupingBlock;
+
+ // Do we have a single Group Block selected and does that group have inner blocks?
+ const _isUngroupable =
+ isSingleGroupingBlock &&
+ !! _blocksSelection[ 0 ].innerBlocks.length;
+ return {
+ clientIds: _clientIds,
+ isGroupable: _isGroupable,
+ isUngroupable: _isUngroupable,
+ blocksSelection: _blocksSelection,
+ groupingBlockName: _groupingBlockName,
+ };
+ }, [] );
+
+ const onConvertToGroup = () => {
+ // Activate the `transform` on the Grouping Block which does the conversion
+ const newBlocks = switchToBlockType(
+ blocksSelection,
+ groupingBlockName
+ );
+ if ( newBlocks ) {
+ replaceBlocks( clientIds, newBlocks );
+ }
+ };
+
+ const onConvertFromGroup = () => {
+ const innerBlocks = blocksSelection[ 0 ].innerBlocks;
+ if ( ! innerBlocks.length ) {
+ return;
+ }
+ replaceBlocks( clientIds, innerBlocks );
+ };
+
+ if ( ! isGroupable && ! isUngroupable ) {
+ return null;
+ }
+
+ return (
+ <>
+ { isGroupable && (
+
+ ) }
+ { isUngroupable && (
+
+ ) }
+ >
+ );
+}
diff --git a/packages/editor/src/components/convert-to-group-buttons/index.native.js b/packages/block-editor/src/components/convert-to-group-buttons/index.native.js
similarity index 100%
rename from packages/editor/src/components/convert-to-group-buttons/index.native.js
rename to packages/block-editor/src/components/convert-to-group-buttons/index.native.js
diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js
index 707f4f2149c4d..1407ad0bdc73d 100644
--- a/packages/block-editor/src/components/iframe/index.js
+++ b/packages/block-editor/src/components/iframe/index.js
@@ -152,13 +152,13 @@ function Iframe( { contentRef, children, head, ...props }, ref ) {
return false;
}
+ contentRef.current = contentDocument.body;
setIframeDocument( contentDocument );
setHead( contentDocument, head );
setBodyClassName( contentDocument );
styleSheetsCompat( contentDocument );
bubbleEvents( contentDocument );
setBodyClassName( contentDocument );
- contentRef.current = contentDocument.body;
return true;
}
diff --git a/packages/block-editor/src/components/inserter-list-item/index.js b/packages/block-editor/src/components/inserter-list-item/index.js
index 5620c31aa0b62..47e48ce8ddf29 100644
--- a/packages/block-editor/src/components/inserter-list-item/index.js
+++ b/packages/block-editor/src/components/inserter-list-item/index.js
@@ -2,13 +2,15 @@
* External dependencies
*/
import classnames from 'classnames';
-import { CompositeItem } from 'reakit';
/**
* WordPress dependencies
*/
import { useMemo, useRef, memo } from '@wordpress/element';
-import { Button } from '@wordpress/components';
+import {
+ Button,
+ __unstableCompositeItem as CompositeItem,
+} from '@wordpress/components';
import {
createBlock,
createBlocksFromInnerBlocksTemplate,
diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js
index 511813003cceb..56f17a95f05f4 100644
--- a/packages/block-editor/src/components/inserter/search-results.js
+++ b/packages/block-editor/src/components/inserter/search-results.js
@@ -153,6 +153,7 @@ function InserterSearchResults( {
onHover,
filterValue,
hasItems,
+ rootClientId: destinationRootClientId,
} }
>
{ ( fills ) => {
diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss
index 478e7ffb71b07..dd2ed5fb6e3fd 100644
--- a/packages/block-editor/src/components/inserter/style.scss
+++ b/packages/block-editor/src/components/inserter/style.scss
@@ -16,6 +16,29 @@ $block-inserter-tabs-height: 44px;
}
}
+.block-editor-inserter__popover.is-quick {
+ .components-popover__content {
+ border: none;
+
+ .block-editor-inserter__quick-inserter > * {
+ border-left: 1px solid $gray-400;
+ border-right: 1px solid $gray-400;
+
+ &:first-child {
+ border-top: 1px solid $gray-400;
+ }
+
+ &:last-child {
+ border-bottom: 1px solid $gray-400;
+ }
+
+ &.components-button {
+ border: 1px solid $gray-900;
+ }
+ }
+ }
+}
+
.block-editor-inserter__popover .block-editor-inserter__menu {
margin: -$grid-unit-15;
@@ -158,7 +181,7 @@ $block-inserter-tabs-height: 44px;
}
.block-editor-inserter__panel-header-patterns {
- padding: $grid-unit-20 $grid-unit-20 0 $grid-unit-10;
+ padding: $grid-unit-20 $grid-unit-20 0;
}
.block-editor-inserter__panel-content {
diff --git a/packages/block-editor/src/components/rich-text/format-toolbar-container.js b/packages/block-editor/src/components/rich-text/format-toolbar-container.js
index 1380a29d7dbae..0d8c88f0761b8 100644
--- a/packages/block-editor/src/components/rich-text/format-toolbar-container.js
+++ b/packages/block-editor/src/components/rich-text/format-toolbar-container.js
@@ -19,6 +19,7 @@ const FormatToolbarContainer = ( { inline, anchorRef } ) => {
focusOnMount={ false }
anchorRef={ anchorRef }
className="block-editor-rich-text__inline-format-toolbar"
+ __unstableSlotName="block-toolbar"
>
diff --git a/packages/block-editor/src/components/rich-text/format-toolbar/index.js b/packages/block-editor/src/components/rich-text/format-toolbar/index.js
index 4e98ce8d064ba..b2cf95204da84 100644
--- a/packages/block-editor/src/components/rich-text/format-toolbar/index.js
+++ b/packages/block-editor/src/components/rich-text/format-toolbar/index.js
@@ -41,9 +41,8 @@ const FormatToolbar = () => {
{ ( toggleProps ) => (
{
@@ -151,7 +153,9 @@ export function onFilesDrop(
const transformation = findTransform(
getBlockTransforms( 'from' ),
( transform ) =>
- transform.type === 'files' && transform.isMatch( files )
+ transform.type === 'files' &&
+ canInsertBlockType( transform.blockName, targetRootClientId ) &&
+ transform.isMatch( files )
);
if ( transformation ) {
@@ -197,17 +201,20 @@ export function onHTMLDrop(
*/
export default function useOnBlockDrop( targetRootClientId, targetBlockIndex ) {
const {
+ canInsertBlockType,
getBlockIndex,
getClientIdsOfDescendants,
hasUploadPermissions,
} = useSelect( ( select ) => {
const {
+ canInsertBlockType: _canInsertBlockType,
getBlockIndex: _getBlockIndex,
getClientIdsOfDescendants: _getClientIdsOfDescendants,
getSettings,
} = select( 'core/block-editor' );
return {
+ canInsertBlockType: _canInsertBlockType,
getBlockIndex: _getBlockIndex,
getClientIdsOfDescendants: _getClientIdsOfDescendants,
hasUploadPermissions: getSettings().mediaUpload,
@@ -236,6 +243,7 @@ export default function useOnBlockDrop( targetRootClientId, targetBlockIndex ) {
targetBlockIndex,
hasUploadPermissions,
updateBlockAttributes,
+ canInsertBlockType,
insertBlocks
),
onHTMLDrop: onHTMLDrop(
diff --git a/packages/block-editor/src/components/use-on-block-drop/test/index.js b/packages/block-editor/src/components/use-on-block-drop/test/index.js
index 964a7cab74b06..e2789bb879473 100644
--- a/packages/block-editor/src/components/use-on-block-drop/test/index.js
+++ b/packages/block-editor/src/components/use-on-block-drop/test/index.js
@@ -305,6 +305,7 @@ describe( 'onBlockDrop', () => {
describe( 'onFilesDrop', () => {
it( 'does nothing if hasUploadPermissions is false', () => {
const updateBlockAttributes = jest.fn();
+ const canInsertBlockType = noop;
const insertBlocks = jest.fn();
const targetRootClientId = '1';
const targetBlockIndex = 0;
@@ -315,6 +316,7 @@ describe( 'onFilesDrop', () => {
targetBlockIndex,
uploadPermissions,
updateBlockAttributes,
+ canInsertBlockType,
insertBlocks
);
onFileDropHandler();
@@ -329,6 +331,7 @@ describe( 'onFilesDrop', () => {
findTransform.mockImplementation( noop );
const updateBlockAttributes = noop;
const insertBlocks = jest.fn();
+ const canInsertBlockType = noop;
const targetRootClientId = '1';
const targetBlockIndex = 0;
const uploadPermissions = true;
@@ -338,6 +341,7 @@ describe( 'onFilesDrop', () => {
targetBlockIndex,
uploadPermissions,
updateBlockAttributes,
+ canInsertBlockType,
insertBlocks
);
onFileDropHandler();
@@ -354,6 +358,7 @@ describe( 'onFilesDrop', () => {
const transformation = { transform: jest.fn( () => blocks ) };
findTransform.mockImplementation( () => transformation );
const updateBlockAttributes = noop;
+ const canInsertBlockType = noop;
const insertBlocks = jest.fn();
const targetRootClientId = '1';
const targetBlockIndex = 0;
@@ -364,6 +369,7 @@ describe( 'onFilesDrop', () => {
targetBlockIndex,
uploadPermissions,
updateBlockAttributes,
+ canInsertBlockType,
insertBlocks
);
const files = 'test';
diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js
index 366d47b7d77f2..9dba003e11bb0 100644
--- a/packages/block-editor/src/store/reducer.js
+++ b/packages/block-editor/src/store/reducer.js
@@ -1174,7 +1174,7 @@ export function isCaretWithinFormattedText( state = false, action ) {
*
* @return {Object} Updated state.
*/
-function selection( state = {}, action ) {
+function selectionHelper( state = {}, action ) {
switch ( action.type ) {
case 'CLEAR_SELECTED_BLOCK': {
if ( state.clientId ) {
@@ -1237,53 +1237,78 @@ function selection( state = {}, action ) {
}
/**
- * Reducer returning the block selection's start.
+ * Reducer returning the selection state.
*
- * @param {Object} state Current state.
- * @param {Object} action Dispatched action.
+ * @param {boolean} state Current state.
+ * @param {Object} action Dispatched action.
*
- * @return {Object} Updated state.
+ * @return {boolean} Updated state.
*/
-export function selectionStart( state = {}, action ) {
+export function selection( state = {}, action ) {
switch ( action.type ) {
case 'SELECTION_CHANGE':
return {
- clientId: action.clientId,
- attributeKey: action.attributeKey,
- offset: action.startOffset,
+ selectionStart: {
+ clientId: action.clientId,
+ attributeKey: action.attributeKey,
+ offset: action.startOffset,
+ },
+ selectionEnd: {
+ clientId: action.clientId,
+ attributeKey: action.attributeKey,
+ offset: action.endOffset,
+ },
};
case 'RESET_SELECTION':
- return action.selectionStart;
- case 'MULTI_SELECT':
- return { clientId: action.start };
- }
-
- return selection( state, action );
-}
-
-/**
- * Reducer returning the block selection's end.
- *
- * @param {Object} state Current state.
- * @param {Object} action Dispatched action.
- *
- * @return {Object} Updated state.
- */
-export function selectionEnd( state = {}, action ) {
- switch ( action.type ) {
- case 'SELECTION_CHANGE':
+ const { selectionStart, selectionEnd } = action;
return {
- clientId: action.clientId,
- attributeKey: action.attributeKey,
- offset: action.endOffset,
+ selectionStart,
+ selectionEnd,
};
- case 'RESET_SELECTION':
- return action.selectionEnd;
case 'MULTI_SELECT':
- return { clientId: action.end };
+ const { start, end } = action;
+ return {
+ selectionStart: { clientId: start },
+ selectionEnd: { clientId: end },
+ };
+ case 'RESET_BLOCKS':
+ const startClientId = state?.selectionStart?.clientId;
+ const endClientId = state?.selectionEnd?.clientId;
+
+ // Do nothing if there's no selected block.
+ if ( ! startClientId && ! endClientId ) {
+ return state;
+ }
+
+ // If the start of the selection won't exist after reset, remove selection.
+ if (
+ ! action.blocks.some(
+ ( block ) => block.clientId === startClientId
+ )
+ ) {
+ return {
+ selectionStart: {},
+ selectionEnd: {},
+ };
+ }
+
+ // If the end of the selection won't exist after reset, collapse selection.
+ if (
+ ! action.blocks.some(
+ ( block ) => block.clientId === endClientId
+ )
+ ) {
+ return {
+ ...state,
+ selectionEnd: state.selectionStart,
+ };
+ }
}
- return selection( state, action );
+ return {
+ selectionStart: selectionHelper( state.selectionStart, action ),
+ selectionEnd: selectionHelper( state.selectionEnd, action ),
+ };
}
/**
@@ -1695,8 +1720,7 @@ export default combineReducers( {
isTyping,
draggedBlocks,
isCaretWithinFormattedText,
- selectionStart,
- selectionEnd,
+ selection,
isMultiSelecting,
isSelectionEnabled,
initialPosition,
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index 764f8350b986d..dc9f27cfb3d06 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -157,7 +157,7 @@ export const getBlock = createSelector(
},
( state, clientId ) => [
// Normally, we'd have both `getBlockAttributes` dependencies and
- // `getBlocks` (children) dependancies here but for performance reasons
+ // `getBlocks` (children) dependencies here but for performance reasons
// we use a denormalized cache key computed in the reducer that takes both
// the attributes and inner blocks into account. The value of the cache key
// is being changed whenever one of these dependencies is out of date.
@@ -370,7 +370,7 @@ export function getBlockCount( state, rootClientId ) {
* @return {WPBlockSelection} Selection start information.
*/
export function getSelectionStart( state ) {
- return state.selectionStart;
+ return state.selection.selectionStart;
}
/**
@@ -382,7 +382,7 @@ export function getSelectionStart( state ) {
* @return {WPBlockSelection} Selection end information.
*/
export function getSelectionEnd( state ) {
- return state.selectionEnd;
+ return state.selection.selectionEnd;
}
/**
@@ -395,7 +395,7 @@ export function getSelectionEnd( state ) {
* @return {?string} Client ID of block selection start.
*/
export function getBlockSelectionStart( state ) {
- return state.selectionStart.clientId;
+ return state.selection.selectionStart.clientId;
}
/**
@@ -408,7 +408,7 @@ export function getBlockSelectionStart( state ) {
* @return {?string} Client ID of block selection end.
*/
export function getBlockSelectionEnd( state ) {
- return state.selectionEnd.clientId;
+ return state.selection.selectionEnd.clientId;
}
/**
@@ -426,7 +426,7 @@ export function getSelectedBlockCount( state ) {
return multiSelectedBlockCount;
}
- return state.selectionStart.clientId ? 1 : 0;
+ return state.selection.selectionStart.clientId ? 1 : 0;
}
/**
@@ -437,7 +437,7 @@ export function getSelectedBlockCount( state ) {
* @return {boolean} Whether a single block is selected.
*/
export function hasSelectedBlock( state ) {
- const { selectionStart, selectionEnd } = state;
+ const { selectionStart, selectionEnd } = state.selection;
return (
!! selectionStart.clientId &&
selectionStart.clientId === selectionEnd.clientId
@@ -453,7 +453,7 @@ export function hasSelectedBlock( state ) {
* @return {?string} Selected block client ID.
*/
export function getSelectedBlockClientId( state ) {
- const { selectionStart, selectionEnd } = state;
+ const { selectionStart, selectionEnd } = state.selection;
const { clientId } = selectionStart;
if ( ! clientId || clientId !== selectionEnd.clientId ) {
@@ -709,7 +709,7 @@ export function getSelectedBlocksInitialCaretPosition( state ) {
*/
export const getSelectedBlockClientIds = createSelector(
( state ) => {
- const { selectionStart, selectionEnd } = state;
+ const { selectionStart, selectionEnd } = state.selection;
if (
selectionStart.clientId === undefined ||
@@ -745,8 +745,8 @@ export const getSelectedBlockClientIds = createSelector(
},
( state ) => [
state.blocks.order,
- state.selectionStart.clientId,
- state.selectionEnd.clientId,
+ state.selection.selectionStart.clientId,
+ state.selection.selectionEnd.clientId,
]
);
@@ -759,7 +759,7 @@ export const getSelectedBlockClientIds = createSelector(
* @return {Array} Multi-selected block client IDs.
*/
export function getMultiSelectedBlockClientIds( state ) {
- const { selectionStart, selectionEnd } = state;
+ const { selectionStart, selectionEnd } = state.selection;
if ( selectionStart.clientId === selectionEnd.clientId ) {
return EMPTY_ARRAY;
@@ -870,8 +870,8 @@ export const isAncestorMultiSelected = createSelector(
},
( state ) => [
state.blocks.order,
- state.selectionStart.clientId,
- state.selectionEnd.clientId,
+ state.selection.selectionStart.clientId,
+ state.selection.selectionEnd.clientId,
]
);
/**
@@ -887,7 +887,7 @@ export const isAncestorMultiSelected = createSelector(
* @return {?string} Client ID of block beginning multi-selection.
*/
export function getMultiSelectedBlocksStartClientId( state ) {
- const { selectionStart, selectionEnd } = state;
+ const { selectionStart, selectionEnd } = state.selection;
if ( selectionStart.clientId === selectionEnd.clientId ) {
return null;
@@ -909,7 +909,7 @@ export function getMultiSelectedBlocksStartClientId( state ) {
* @return {?string} Client ID of block ending multi-selection.
*/
export function getMultiSelectedBlocksEndClientId( state ) {
- const { selectionStart, selectionEnd } = state;
+ const { selectionStart, selectionEnd } = state.selection;
if ( selectionStart.clientId === selectionEnd.clientId ) {
return null;
@@ -956,7 +956,7 @@ export function getBlockIndex( state, clientId, rootClientId ) {
* @return {boolean} Whether block is selected and multi-selection exists.
*/
export function isBlockSelected( state, clientId ) {
- const { selectionStart, selectionEnd } = state;
+ const { selectionStart, selectionEnd } = state.selection;
if ( selectionStart.clientId !== selectionEnd.clientId ) {
return false;
@@ -1014,7 +1014,7 @@ export function isBlockWithinSelection( state, clientId ) {
* @return {boolean} Whether multi-selection has been made.
*/
export function hasMultiSelection( state ) {
- const { selectionStart, selectionEnd } = state;
+ const { selectionStart, selectionEnd } = state.selection;
return selectionStart.clientId !== selectionEnd.clientId;
}
@@ -1157,7 +1157,10 @@ export function isCaretWithinFormattedText( state ) {
export function getBlockInsertionPoint( state ) {
let rootClientId, index;
- const { insertionPoint, selectionEnd } = state;
+ const {
+ insertionPoint,
+ selection: { selectionEnd },
+ } = state;
if ( insertionPoint !== null ) {
return insertionPoint;
}
@@ -1233,9 +1236,11 @@ export function getTemplateLock( state, rootClientId ) {
* This function is not exported and not memoized because using a memoized selector
* inside another memoized selector is just a waste of time.
*
- * @param {Object} state Editor state.
- * @param {string} blockName The name of the block type, e.g.' core/paragraph'.
- * @param {?string} rootClientId Optional root client ID of block list.
+ * @param {Object} state Editor state.
+ * @param {string|Object} blockName The block type object, e.g., the response
+ * from the block directory; or a string name of
+ * an installed block type, e.g.' core/paragraph'.
+ * @param {?string} rootClientId Optional root client ID of block list.
*
* @return {boolean} Whether the given block type is allowed to be inserted.
*/
@@ -1260,7 +1265,13 @@ const canInsertBlockTypeUnmemoized = (
return defaultResult;
};
- const blockType = getBlockType( blockName );
+ let blockType;
+ if ( blockName && 'object' === typeof blockName ) {
+ blockType = blockName;
+ blockName = blockType.name;
+ } else {
+ blockType = getBlockType( blockName );
+ }
if ( ! blockType ) {
return false;
}
@@ -1944,8 +1955,8 @@ export const __experimentalGetActiveBlockIdByBlockNames = createSelector(
return null;
},
( state, validBlockNames ) => [
- state.selectionStart.clientId,
- state.selectionEnd.clientId,
+ state.selection.selectionStart.clientId,
+ state.selection.selectionEnd.clientId,
validBlockNames,
]
);
diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js
index d9598b9a7e6b8..8bbc93386e277 100644
--- a/packages/block-editor/src/store/test/actions.js
+++ b/packages/block-editor/src/store/test/actions.js
@@ -1,12 +1,27 @@
+/**
+ * External dependencies
+ */
+import { noop } from 'lodash';
+import deepFreeze from 'deep-freeze';
+
/**
* WordPress dependencies
*/
-import { controls } from '@wordpress/data';
+import { controls, createRegistry } from '@wordpress/data';
+import {
+ getBlockTypes,
+ unregisterBlockType,
+ registerBlockType,
+ createBlock,
+} from '@wordpress/blocks';
/**
* Internal dependencies
*/
-import {
+
+import * as selectors from '../selectors';
+import reducer from '../reducer';
+import actions, {
clearSelectedBlock,
enterFormattedText,
exitFormattedText,
@@ -37,9 +52,22 @@ import {
updateBlock,
updateBlockAttributes,
updateBlockListSettings,
+ updateSettings,
+ selectionChange,
+ validateBlocksToTemplate,
} from '../actions';
+import '../..';
describe( 'actions', () => {
+ const defaultBlockSettings = {
+ attributes: {
+ content: {},
+ },
+ save: () => 'Saved',
+ category: 'text',
+ title: 'block title',
+ };
+
describe( 'resetBlocks', () => {
it( 'should yield the RESET_BLOCKS actions', () => {
const blocks = [];
@@ -737,29 +765,14 @@ describe( 'actions', () => {
} );
} );
- describe( 'mergeBlocks', () => {
- it( 'should return MERGE_BLOCKS action', () => {
- const firstBlockClientId = 'blockA';
- const secondBlockClientId = 'blockB';
- const fulfillment = mergeBlocks(
- firstBlockClientId,
- secondBlockClientId
- );
- expect( fulfillment.next().value ).toEqual( {
- type: 'MERGE_BLOCKS',
- blocks: [ firstBlockClientId, secondBlockClientId ],
- } );
- } );
- } );
-
describe( 'removeBlocks', () => {
it( 'should return REMOVE_BLOCKS action', () => {
const clientId = 'clientId';
const clientIds = [ clientId ];
- const actions = Array.from( removeBlocks( clientIds ) );
+ const result = Array.from( removeBlocks( clientIds ) );
- expect( actions ).toEqual( [
+ expect( result ).toEqual( [
controls.select(
'core/block-editor',
'getBlockRootClientId',
@@ -958,9 +971,9 @@ describe( 'actions', () => {
it( 'should return REMOVE_BLOCKS action', () => {
const clientId = 'myclientid';
- const actions = Array.from( removeBlock( clientId ) );
+ const result = Array.from( removeBlock( clientId ) );
- expect( actions ).toEqual( [
+ expect( result ).toEqual( [
controls.select(
'core/block-editor',
'getBlockRootClientId',
@@ -983,9 +996,9 @@ describe( 'actions', () => {
it( 'should return REMOVE_BLOCKS action, opting out of select previous', () => {
const clientId = 'myclientid';
- const actions = Array.from( removeBlock( clientId, false ) );
+ const result = Array.from( removeBlock( clientId, false ) );
- expect( actions ).toEqual( [
+ expect( result ).toEqual( [
controls.select(
'core/block-editor',
'getBlockRootClientId',
@@ -1138,4 +1151,347 @@ describe( 'actions', () => {
} );
} );
} );
+
+ describe( 'mergeBlocks', () => {
+ afterEach( () => {
+ getBlockTypes().forEach( ( block ) => {
+ unregisterBlockType( block.name );
+ } );
+ } );
+
+ it( 'should return MERGE_BLOCKS action', () => {
+ const firstBlockClientId = 'blockA';
+ const secondBlockClientId = 'blockB';
+ const fulfillment = mergeBlocks(
+ firstBlockClientId,
+ secondBlockClientId
+ );
+ expect( fulfillment.next().value ).toEqual( {
+ type: 'MERGE_BLOCKS',
+ blocks: [ firstBlockClientId, secondBlockClientId ],
+ } );
+ } );
+
+ it( 'should only focus the blockA if the blockA has no merge function', () => {
+ registerBlockType( 'core/test-block', defaultBlockSettings );
+ const blockA = deepFreeze( {
+ clientId: 'chicken',
+ name: 'core/test-block',
+ } );
+ const blockB = deepFreeze( {
+ clientId: 'ribs',
+ name: 'core/test-block',
+ } );
+
+ const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId );
+ expect( fulfillment.next() ).toEqual( {
+ done: false,
+ value: {
+ type: 'MERGE_BLOCKS',
+ blocks: [ blockA.clientId, blockB.clientId ],
+ },
+ } );
+ fulfillment.next();
+ expect( fulfillment.next( blockA ) ).toEqual( {
+ done: false,
+ value: selectBlock( 'chicken' ),
+ } );
+ expect( fulfillment.next( blockA ).done ).toEqual( true );
+ } );
+
+ it( 'should merge the blocks if blocks of the same type', () => {
+ registerBlockType( 'core/test-block', {
+ attributes: {
+ content: {},
+ },
+ merge( attributes, attributesToMerge ) {
+ return {
+ content:
+ attributes.content +
+ ' ' +
+ attributesToMerge.content,
+ };
+ },
+ save: noop,
+ category: 'text',
+ title: 'test block',
+ } );
+ const blockA = deepFreeze( {
+ clientId: 'chicken',
+ name: 'core/test-block',
+ attributes: { content: 'chicken' },
+ innerBlocks: [],
+ } );
+ const blockB = deepFreeze( {
+ clientId: 'ribs',
+ name: 'core/test-block',
+ attributes: { content: 'ribs' },
+ innerBlocks: [],
+ } );
+
+ const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId );
+ // MERGE_BLOCKS
+ fulfillment.next();
+ // getBlock A
+ fulfillment.next();
+ fulfillment.next( blockA );
+ // getBlock B
+ fulfillment.next( blockB );
+ // getSelectionStart
+ fulfillment.next( {
+ clientId: blockB.clientId,
+ attributeKey: 'content',
+ offset: 0,
+ } );
+ // selectionChange
+ fulfillment.next(
+ selectionChange(
+ blockA.clientId,
+ 'content',
+ 'chicken'.length + 1,
+ 'chicken'.length + 1
+ )
+ );
+ fulfillment.next();
+ fulfillment.next();
+ expect( fulfillment.next( blockA ).value ).toMatchObject( {
+ type: 'REPLACE_BLOCKS',
+ clientIds: [ 'chicken', 'ribs' ],
+ blocks: [
+ {
+ clientId: 'chicken',
+ name: 'core/test-block',
+ attributes: { content: 'chicken ribs' },
+ },
+ ],
+ } );
+ } );
+
+ it( 'should not merge the blocks have different types without transformation', () => {
+ registerBlockType( 'core/test-block', {
+ attributes: {
+ content: {},
+ },
+ merge( attributes, attributesToMerge ) {
+ return {
+ content:
+ attributes.content +
+ ' ' +
+ attributesToMerge.content,
+ };
+ },
+ save: noop,
+ category: 'text',
+ title: 'test block',
+ } );
+ registerBlockType( 'core/test-block-2', defaultBlockSettings );
+ const blockA = deepFreeze( {
+ clientId: 'chicken',
+ name: 'core/test-block',
+ attributes: { content: 'chicken' },
+ innerBlocks: [],
+ } );
+ const blockB = deepFreeze( {
+ clientId: 'ribs',
+ name: 'core/test-block-2',
+ attributes: { content: 'ribs' },
+ innerBlocks: [],
+ } );
+
+ const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId );
+ // MERGE_BLOCKS
+ fulfillment.next();
+ // getBlock A
+ fulfillment.next();
+ fulfillment.next( blockA );
+ // getBlock B
+ expect( fulfillment.next( blockB ).value ).toEqual( {
+ args: [],
+ selectorName: 'getSelectionStart',
+ storeKey: 'core/block-editor',
+ type: '@@data/SELECT',
+ } );
+ // getSelectionStart
+ const next = fulfillment.next( {
+ clientId: blockB.clientId,
+ attributeKey: 'content',
+ offset: 0,
+ } );
+ expect( next.value ).toEqual( undefined );
+ expect( next.done ).toBe( true );
+ } );
+
+ it( 'should transform and merge the blocks', () => {
+ registerBlockType( 'core/test-block', {
+ attributes: {
+ content: {
+ type: 'string',
+ },
+ },
+ merge( attributes, attributesToMerge ) {
+ return {
+ content:
+ attributes.content +
+ ' ' +
+ attributesToMerge.content,
+ };
+ },
+ save: noop,
+ category: 'text',
+ title: 'test block',
+ } );
+ registerBlockType( 'core/test-block-2', {
+ attributes: {
+ content2: {
+ type: 'string',
+ },
+ },
+ transforms: {
+ to: [
+ {
+ type: 'block',
+ blocks: [ 'core/test-block' ],
+ transform: ( { content2 } ) => {
+ return createBlock( 'core/test-block', {
+ content: content2,
+ } );
+ },
+ },
+ ],
+ },
+ save: noop,
+ category: 'text',
+ title: 'test block 2',
+ } );
+ const blockA = deepFreeze( {
+ clientId: 'chicken',
+ name: 'core/test-block',
+ attributes: { content: 'chicken' },
+ innerBlocks: [],
+ } );
+ const blockB = deepFreeze( {
+ clientId: 'ribs',
+ name: 'core/test-block-2',
+ attributes: { content2: 'ribs' },
+ innerBlocks: [],
+ } );
+
+ const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId );
+ // MERGE_BLOCKS
+ fulfillment.next();
+ // getBlock A
+ fulfillment.next();
+ fulfillment.next( blockA );
+ // getBlock B
+ expect( fulfillment.next( blockB ).value ).toEqual( {
+ args: [],
+ selectorName: 'getSelectionStart',
+ storeKey: 'core/block-editor',
+ type: '@@data/SELECT',
+ } );
+ expect(
+ fulfillment.next( {
+ clientId: blockB.clientId,
+ attributeKey: 'content2',
+ offset: 0,
+ } ).value
+ ).toEqual(
+ selectionChange(
+ blockA.clientId,
+ 'content',
+ 'chicken'.length + 1,
+ 'chicken'.length + 1
+ )
+ );
+
+ fulfillment.next();
+ fulfillment.next();
+ fulfillment.next();
+ expect( fulfillment.next( blockA ).value ).toMatchObject( {
+ type: 'REPLACE_BLOCKS',
+ clientIds: [ 'chicken', 'ribs' ],
+ blocks: [
+ {
+ clientId: 'chicken',
+ name: 'core/test-block',
+ attributes: { content: 'chicken ribs' },
+ },
+ ],
+ } );
+ } );
+ } );
+
+ describe( 'validateBlocksToTemplate', () => {
+ let store;
+ beforeEach( () => {
+ store = createRegistry().registerStore( 'core/block-editor', {
+ actions,
+ selectors,
+ reducer,
+ } );
+
+ registerBlockType( 'core/test-block', defaultBlockSettings );
+ } );
+
+ afterEach( () => {
+ getBlockTypes().forEach( ( block ) => {
+ unregisterBlockType( block.name );
+ } );
+ } );
+
+ it( 'should return undefined if no template assigned', async () => {
+ const result = await store.dispatch(
+ validateBlocksToTemplate(
+ resetBlocks( [ createBlock( 'core/test-block' ) ] ),
+ store
+ )
+ );
+
+ expect( result ).toEqual( undefined );
+ } );
+
+ it( 'should return undefined if invalid but unlocked', async () => {
+ store.dispatch(
+ updateSettings( {
+ template: [ [ 'core/foo', {} ] ],
+ } )
+ );
+
+ const result = await store.dispatch(
+ validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] )
+ );
+
+ expect( result ).toEqual( undefined );
+ } );
+
+ it( 'should return undefined if locked and valid', async () => {
+ store.dispatch(
+ updateSettings( {
+ template: [ [ 'core/test-block' ] ],
+ templateLock: 'all',
+ } )
+ );
+
+ const result = await store.dispatch(
+ validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] )
+ );
+
+ expect( result ).toEqual( undefined );
+ } );
+
+ it( 'should return validity set action if invalid on default state', async () => {
+ store.dispatch(
+ updateSettings( {
+ template: [ [ 'core/foo' ] ],
+ templateLock: 'all',
+ } )
+ );
+
+ const result = await store.dispatch(
+ validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] )
+ );
+
+ expect( result ).toEqual( false );
+ } );
+ } );
} );
diff --git a/packages/block-editor/src/store/test/effects.js b/packages/block-editor/src/store/test/effects.js
deleted file mode 100644
index 9a8ecc4980187..0000000000000
--- a/packages/block-editor/src/store/test/effects.js
+++ /dev/null
@@ -1,372 +0,0 @@
-/**
- * External dependencies
- */
-import { noop } from 'lodash';
-import deepFreeze from 'deep-freeze';
-
-/**
- * WordPress dependencies
- */
-import {
- getBlockTypes,
- unregisterBlockType,
- registerBlockType,
- createBlock,
-} from '@wordpress/blocks';
-import { createRegistry } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import actions, {
- updateSettings,
- mergeBlocks,
- resetBlocks,
- selectBlock,
- selectionChange,
- validateBlocksToTemplate,
-} from '../actions';
-import * as selectors from '../selectors';
-import reducer from '../reducer';
-import '../../';
-
-describe( 'effects', () => {
- const defaultBlockSettings = {
- attributes: {
- content: {},
- },
- save: () => 'Saved',
- category: 'text',
- title: 'block title',
- };
-
- describe( '.MERGE_BLOCKS', () => {
- afterEach( () => {
- getBlockTypes().forEach( ( block ) => {
- unregisterBlockType( block.name );
- } );
- } );
-
- it( 'should only focus the blockA if the blockA has no merge function', () => {
- registerBlockType( 'core/test-block', defaultBlockSettings );
- const blockA = deepFreeze( {
- clientId: 'chicken',
- name: 'core/test-block',
- } );
- const blockB = deepFreeze( {
- clientId: 'ribs',
- name: 'core/test-block',
- } );
-
- const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId );
- expect( fulfillment.next() ).toEqual( {
- done: false,
- value: {
- type: 'MERGE_BLOCKS',
- blocks: [ blockA.clientId, blockB.clientId ],
- },
- } );
- fulfillment.next();
- expect( fulfillment.next( blockA ) ).toEqual( {
- done: false,
- value: selectBlock( 'chicken' ),
- } );
- expect( fulfillment.next( blockA ).done ).toEqual( true );
- } );
-
- it( 'should merge the blocks if blocks of the same type', () => {
- registerBlockType( 'core/test-block', {
- attributes: {
- content: {},
- },
- merge( attributes, attributesToMerge ) {
- return {
- content:
- attributes.content +
- ' ' +
- attributesToMerge.content,
- };
- },
- save: noop,
- category: 'text',
- title: 'test block',
- } );
- const blockA = deepFreeze( {
- clientId: 'chicken',
- name: 'core/test-block',
- attributes: { content: 'chicken' },
- innerBlocks: [],
- } );
- const blockB = deepFreeze( {
- clientId: 'ribs',
- name: 'core/test-block',
- attributes: { content: 'ribs' },
- innerBlocks: [],
- } );
-
- const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId );
- // MERGE_BLOCKS
- fulfillment.next();
- // getBlock A
- fulfillment.next();
- fulfillment.next( blockA );
- // getBlock B
- fulfillment.next( blockB );
- // getSelectionStart
- fulfillment.next( {
- clientId: blockB.clientId,
- attributeKey: 'content',
- offset: 0,
- } );
- // selectionChange
- fulfillment.next(
- selectionChange(
- blockA.clientId,
- 'content',
- 'chicken'.length + 1,
- 'chicken'.length + 1
- )
- );
- fulfillment.next();
- fulfillment.next();
- expect( fulfillment.next( blockA ).value ).toMatchObject( {
- type: 'REPLACE_BLOCKS',
- clientIds: [ 'chicken', 'ribs' ],
- blocks: [
- {
- clientId: 'chicken',
- name: 'core/test-block',
- attributes: { content: 'chicken ribs' },
- },
- ],
- } );
- } );
-
- it( 'should not merge the blocks have different types without transformation', () => {
- registerBlockType( 'core/test-block', {
- attributes: {
- content: {},
- },
- merge( attributes, attributesToMerge ) {
- return {
- content:
- attributes.content +
- ' ' +
- attributesToMerge.content,
- };
- },
- save: noop,
- category: 'text',
- title: 'test block',
- } );
- registerBlockType( 'core/test-block-2', defaultBlockSettings );
- const blockA = deepFreeze( {
- clientId: 'chicken',
- name: 'core/test-block',
- attributes: { content: 'chicken' },
- innerBlocks: [],
- } );
- const blockB = deepFreeze( {
- clientId: 'ribs',
- name: 'core/test-block-2',
- attributes: { content: 'ribs' },
- innerBlocks: [],
- } );
-
- const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId );
- // MERGE_BLOCKS
- fulfillment.next();
- // getBlock A
- fulfillment.next();
- fulfillment.next( blockA );
- // getBlock B
- expect( fulfillment.next( blockB ).value ).toEqual( {
- args: [],
- selectorName: 'getSelectionStart',
- storeKey: 'core/block-editor',
- type: '@@data/SELECT',
- } );
- // getSelectionStart
- const next = fulfillment.next( {
- clientId: blockB.clientId,
- attributeKey: 'content',
- offset: 0,
- } );
- expect( next.value ).toEqual( undefined );
- expect( next.done ).toBe( true );
- } );
-
- it( 'should transform and merge the blocks', () => {
- registerBlockType( 'core/test-block', {
- attributes: {
- content: {
- type: 'string',
- },
- },
- merge( attributes, attributesToMerge ) {
- return {
- content:
- attributes.content +
- ' ' +
- attributesToMerge.content,
- };
- },
- save: noop,
- category: 'text',
- title: 'test block',
- } );
- registerBlockType( 'core/test-block-2', {
- attributes: {
- content2: {
- type: 'string',
- },
- },
- transforms: {
- to: [
- {
- type: 'block',
- blocks: [ 'core/test-block' ],
- transform: ( { content2 } ) => {
- return createBlock( 'core/test-block', {
- content: content2,
- } );
- },
- },
- ],
- },
- save: noop,
- category: 'text',
- title: 'test block 2',
- } );
- const blockA = deepFreeze( {
- clientId: 'chicken',
- name: 'core/test-block',
- attributes: { content: 'chicken' },
- innerBlocks: [],
- } );
- const blockB = deepFreeze( {
- clientId: 'ribs',
- name: 'core/test-block-2',
- attributes: { content2: 'ribs' },
- innerBlocks: [],
- } );
-
- const fulfillment = mergeBlocks( blockA.clientId, blockB.clientId );
- // MERGE_BLOCKS
- fulfillment.next();
- // getBlock A
- fulfillment.next();
- fulfillment.next( blockA );
- // getBlock B
- expect( fulfillment.next( blockB ).value ).toEqual( {
- args: [],
- selectorName: 'getSelectionStart',
- storeKey: 'core/block-editor',
- type: '@@data/SELECT',
- } );
- expect(
- fulfillment.next( {
- clientId: blockB.clientId,
- attributeKey: 'content2',
- offset: 0,
- } ).value
- ).toEqual(
- selectionChange(
- blockA.clientId,
- 'content',
- 'chicken'.length + 1,
- 'chicken'.length + 1
- )
- );
-
- fulfillment.next();
- fulfillment.next();
- fulfillment.next();
- expect( fulfillment.next( blockA ).value ).toMatchObject( {
- type: 'REPLACE_BLOCKS',
- clientIds: [ 'chicken', 'ribs' ],
- blocks: [
- {
- clientId: 'chicken',
- name: 'core/test-block',
- attributes: { content: 'chicken ribs' },
- },
- ],
- } );
- } );
- } );
-
- describe( 'validateBlocksToTemplate', () => {
- let store;
- beforeEach( () => {
- store = createRegistry().registerStore( 'core/block-editor', {
- actions,
- selectors,
- reducer,
- } );
-
- registerBlockType( 'core/test-block', defaultBlockSettings );
- } );
-
- afterEach( () => {
- getBlockTypes().forEach( ( block ) => {
- unregisterBlockType( block.name );
- } );
- } );
-
- it( 'should return undefined if no template assigned', async () => {
- const result = await store.dispatch(
- validateBlocksToTemplate(
- resetBlocks( [ createBlock( 'core/test-block' ) ] ),
- store
- )
- );
-
- expect( result ).toEqual( undefined );
- } );
-
- it( 'should return undefined if invalid but unlocked', async () => {
- store.dispatch(
- updateSettings( {
- template: [ [ 'core/foo', {} ] ],
- } )
- );
-
- const result = await store.dispatch(
- validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] )
- );
-
- expect( result ).toEqual( undefined );
- } );
-
- it( 'should return undefined if locked and valid', async () => {
- store.dispatch(
- updateSettings( {
- template: [ [ 'core/test-block' ] ],
- templateLock: 'all',
- } )
- );
-
- const result = await store.dispatch(
- validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] )
- );
-
- expect( result ).toEqual( undefined );
- } );
-
- it( 'should return validity set action if invalid on default state', async () => {
- store.dispatch(
- updateSettings( {
- template: [ [ 'core/foo' ] ],
- templateLock: 'all',
- } )
- );
-
- const result = await store.dispatch(
- validateBlocksToTemplate( [ createBlock( 'core/test-block' ) ] )
- );
-
- expect( result ).toEqual( false );
- } );
- } );
-} );
diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js
index 3906c5c4c4c1f..a13e00c8c8f6a 100644
--- a/packages/block-editor/src/store/test/reducer.js
+++ b/packages/block-editor/src/store/test/reducer.js
@@ -23,8 +23,7 @@ import {
isTyping,
draggedBlocks,
isCaretWithinFormattedText,
- selectionStart,
- selectionEnd,
+ selection,
initialPosition,
isMultiSelecting,
preferences,
@@ -2181,18 +2180,17 @@ describe( 'state', () => {
} );
} );
- describe( 'selectionStart() and selectionEnd()', () => {
+ describe( 'selection()', () => {
it( 'should set multi selection', () => {
const action = {
type: 'MULTI_SELECT',
start: 'ribs',
end: 'chicken',
};
- const state1 = selectionStart( undefined, action );
- const state2 = selectionEnd( undefined, action );
+ const state = selection( undefined, action );
- expect( state1 ).toEqual( { clientId: 'ribs' } );
- expect( state2 ).toEqual( { clientId: 'chicken' } );
+ expect( state.selectionStart ).toEqual( { clientId: 'ribs' } );
+ expect( state.selectionEnd ).toEqual( { clientId: 'chicken' } );
} );
it( 'should reset selection', () => {
@@ -2203,11 +2201,10 @@ describe( 'state', () => {
selectionStart: start,
selectionEnd: end,
};
- const state1 = selectionStart( undefined, action );
- const state2 = selectionEnd( undefined, action );
+ const state = selection( undefined, action );
- expect( state1 ).toEqual( start );
- expect( state2 ).toEqual( end );
+ expect( state.selectionStart ).toEqual( start );
+ expect( state.selectionEnd ).toEqual( end );
} );
it( 'should change selection', () => {
@@ -2218,15 +2215,14 @@ describe( 'state', () => {
startOffset: 1,
endOffset: 2,
};
- const state1 = selectionStart( undefined, action );
- const state2 = selectionEnd( undefined, action );
+ const state = selection( undefined, action );
- expect( state1 ).toEqual( {
+ expect( state.selectionStart ).toEqual( {
clientId: 'a',
attributeKey: 'content',
offset: 1,
} );
- expect( state2 ).toEqual( {
+ expect( state.selectionEnd ).toEqual( {
clientId: 'a',
attributeKey: 'content',
offset: 2,
@@ -2238,51 +2234,58 @@ describe( 'state', () => {
type: 'SELECT_BLOCK',
clientId: 'kumquat',
};
- const state1 = selectionStart( undefined, action );
- const state2 = selectionEnd( undefined, action );
+ const state = selection( undefined, action );
const expected = { clientId: 'kumquat' };
- expect( state1 ).toEqual( expected );
- expect( state2 ).toEqual( expected );
+ expect( state.selectionStart ).toEqual( expected );
+ expect( state.selectionEnd ).toEqual( expected );
} );
it( 'should not update the state if the block is already selected', () => {
- const original = deepFreeze( { clientId: 'ribs' } );
+ const clientId = 'ribs';
+ const original = deepFreeze( {
+ selectionStart: { clientId },
+ selectionEnd: { clientId },
+ } );
const action = {
type: 'SELECT_BLOCK',
- clientId: 'ribs',
+ clientId,
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toBe( original );
- expect( state2 ).toBe( original );
+ expect( state.selectionStart ).toBe( original.selectionStart );
+ expect( state.selectionEnd ).toBe( original.selectionEnd );
} );
it( 'should unset selection', () => {
- const original = deepFreeze( { clientId: 'ribs' } );
+ const clientId = 'ribs';
+ const original = deepFreeze( {
+ selectionStart: { clientId },
+ selectionEnd: { clientId },
+ } );
const action = {
type: 'CLEAR_SELECTED_BLOCK',
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toEqual( {} );
- expect( state2 ).toEqual( {} );
+ expect( state.selectionStart ).toEqual( {} );
+ expect( state.selectionEnd ).toEqual( {} );
} );
it( 'should return same reference if clearing selection but no selection', () => {
- const original = deepFreeze( {} );
+ const original = deepFreeze( {
+ selectionStart: {},
+ selectionEnd: {},
+ } );
const action = {
type: 'CLEAR_SELECTED_BLOCK',
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toBe( original );
- expect( state2 ).toBe( original );
+ expect( state.selectionStart ).toBe( original.selectionStart );
+ expect( state.selectionEnd ).toBe( original.selectionEnd );
} );
it( 'should select inserted block', () => {
@@ -2291,172 +2294,259 @@ describe( 'state', () => {
blocks: [ { clientId: 'ribs' } ],
updateSelection: true,
};
- const state1 = selectionStart( undefined, action );
- const state2 = selectionEnd( undefined, action );
+ const state = selection( undefined, action );
const expected = { clientId: 'ribs' };
- expect( state1 ).toEqual( expected );
- expect( state2 ).toEqual( expected );
+ expect( state.selectionStart ).toEqual( expected );
+ expect( state.selectionEnd ).toEqual( expected );
} );
it( 'should not select inserted block if updateSelection flag is false', () => {
- const original = deepFreeze( { clientId: 'a' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'a' },
+ selectionEnd: { clientId: 'a' },
+ } );
const action = {
type: 'INSERT_BLOCKS',
blocks: [ { clientId: 'ribs' } ],
updateSelection: false,
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toBe( original );
- expect( state2 ).toBe( original );
+ expect( state.selectionStart ).toBe( original.selectionStart );
+ expect( state.selectionEnd ).toBe( original.selectionEnd );
} );
it( 'should not update the state if the block moved is already selected', () => {
- const original = deepFreeze( { clientId: 'ribs' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'ribs' },
+ selectionEnd: { clientId: 'ribs' },
+ } );
const action = {
type: 'MOVE_BLOCKS_UP',
clientIds: [ 'ribs' ],
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toBe( original );
- expect( state2 ).toBe( original );
+ expect( state.selectionStart ).toBe( original.selectionStart );
+ expect( state.selectionEnd ).toBe( original.selectionEnd );
} );
it( 'should replace the selected block', () => {
- const original = deepFreeze( { clientId: 'chicken' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'chicken' },
+ } );
const action = {
type: 'REPLACE_BLOCKS',
clientIds: [ 'chicken' ],
blocks: [ { clientId: 'wings' } ],
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
- const expected = { clientId: 'wings' };
+ const state = selection( original, action );
+ const expected = {
+ selectionStart: { clientId: 'wings' },
+ selectionEnd: { clientId: 'wings' },
+ };
- expect( state1 ).toEqual( expected );
- expect( state2 ).toEqual( expected );
+ expect( state.selectionStart ).toEqual( expected.selectionStart );
+ expect( state.selectionEnd ).toEqual( expected.selectionEnd );
} );
it( 'should not replace the selected block if we keep it at the end when replacing blocks', () => {
- const original = deepFreeze( { clientId: 'wings' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'wings' },
+ selectionEnd: { clientId: 'wings' },
+ } );
const action = {
type: 'REPLACE_BLOCKS',
clientIds: [ 'wings' ],
blocks: [ { clientId: 'chicken' }, { clientId: 'wings' } ],
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toBe( original );
- expect( state2 ).toBe( original );
+ expect( state.selectionStart ).toBe( original.selectionStart );
+ expect( state.selectionEnd ).toBe( original.selectionEnd );
} );
it( 'should replace the selected block if we keep it not at the end when replacing blocks', () => {
- const original = deepFreeze( { clientId: 'chicken' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'chicken' },
+ } );
const action = {
type: 'REPLACE_BLOCKS',
clientIds: [ 'chicken' ],
blocks: [ { clientId: 'chicken' }, { clientId: 'wings' } ],
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
- const expected = { clientId: 'wings' };
+ const state = selection( original, action );
+ const expected = {
+ selectionStart: { clientId: 'wings' },
+ selectionEnd: { clientId: 'wings' },
+ };
- expect( state1 ).toEqual( expected );
- expect( state2 ).toEqual( expected );
+ expect( state.selectionStart ).toEqual( expected.selectionStart );
+ expect( state.selectionEnd ).toEqual( expected.selectionEnd );
} );
it( 'should reset if replacing with empty set', () => {
- const original = deepFreeze( { clientId: 'chicken' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'chicken' },
+ } );
const action = {
type: 'REPLACE_BLOCKS',
clientIds: [ 'chicken' ],
blocks: [],
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toEqual( {} );
- expect( state2 ).toEqual( {} );
+ expect( state.selectionStart ).toEqual( {} );
+ expect( state.selectionEnd ).toEqual( {} );
} );
it( 'should keep the selected block', () => {
- const original = deepFreeze( { clientId: 'chicken' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'chicken' },
+ } );
const action = {
type: 'REPLACE_BLOCKS',
clientIds: [ 'ribs' ],
blocks: [ { clientId: 'wings' } ],
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toBe( original );
- expect( state2 ).toBe( original );
+ expect( state.selectionStart ).toBe( original.selectionStart );
+ expect( state.selectionEnd ).toBe( original.selectionEnd );
} );
it( 'should remove the selection if we are removing the selected block', () => {
- const original = deepFreeze( { clientId: 'chicken' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'chicken' },
+ } );
const action = {
type: 'REMOVE_BLOCKS',
clientIds: [ 'chicken' ],
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toEqual( {} );
- expect( state2 ).toEqual( {} );
+ expect( state.selectionStart ).toEqual( {} );
+ expect( state.selectionEnd ).toEqual( {} );
} );
it( 'should keep the selection if we are not removing the selected block', () => {
- const original = deepFreeze( { clientId: 'chicken' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'chicken' },
+ } );
const action = {
type: 'REMOVE_BLOCKS',
clientIds: [ 'ribs' ],
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
- expect( state1 ).toEqual( original );
- expect( state2 ).toEqual( original );
+ expect( state.selectionStart ).toEqual( original.selectionStart );
+ expect( state.selectionEnd ).toEqual( original.selectionEnd );
} );
it( 'should update the selection on inner blocks replace if updateSelection is true', () => {
- const original = deepFreeze( { clientId: 'chicken' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'chicken' },
+ } );
const action = {
type: 'REPLACE_INNER_BLOCKS',
blocks: [ { clientId: 'another-block' } ],
rootClientId: 'parent',
updateSelection: true,
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
- const expected = { clientId: 'another-block' };
+ const state = selection( original, action );
+ const expected = {
+ selectionStart: { clientId: 'another-block' },
+ selectionEnd: { clientId: 'another-block' },
+ };
- expect( state1 ).toEqual( expected );
- expect( state2 ).toEqual( expected );
+ expect( state.selectionStart ).toEqual( expected.selectionStart );
+ expect( state.selectionEnd ).toEqual( expected.selectionEnd );
} );
it( 'should not update the selection on inner blocks replace if updateSelection is false', () => {
- const original = deepFreeze( { clientId: 'chicken' } );
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'chicken' },
+ } );
const action = {
type: 'REPLACE_INNER_BLOCKS',
blocks: [ { clientId: 'another-block' } ],
rootClientId: 'parent',
updateSelection: false,
};
- const state1 = selectionStart( original, action );
- const state2 = selectionEnd( original, action );
+ const state = selection( original, action );
+
+ expect( state.selectionStart ).toEqual( original.selectionStart );
+ expect( state.selectionEnd ).toEqual( original.selectionEnd );
+ } );
+
+ it( 'should keep the same selection on RESET_BLOCKS if the selected blocks continue to exist', () => {
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'cow' },
+ } );
+ const action = {
+ type: 'RESET_BLOCKS',
+ blocks: [
+ { clientId: 'another-block' },
+ { clientId: 'chicken' },
+ { clientId: 'this-is-a-block-too' },
+ { clientId: 'cow' },
+ ],
+ };
+ const state = selection( original, action );
+
+ expect( state.selectionStart ).toEqual( original.selectionStart );
+ expect( state.selectionEnd ).toEqual( original.selectionEnd );
+ } );
+
+ it( 'should collapse the selection on RESET_BLOCKS if the selection start exists, but the end does not', () => {
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'cow' },
+ } );
+ const action = {
+ type: 'RESET_BLOCKS',
+ blocks: [
+ { clientId: 'another-block' },
+ { clientId: 'chicken' },
+ { clientId: 'this-is-a-block-too' },
+ ],
+ };
+ const state = selection( original, action );
+
+ expect( state.selectionStart ).toEqual( original.selectionStart );
+ expect( state.selectionEnd ).toEqual( original.selectionStart );
+ } );
+
+ it( 'should clear the selection on RESET_BLOCKS if the blocks currently selected are removed', () => {
+ const original = deepFreeze( {
+ selectionStart: { clientId: 'chicken' },
+ selectionEnd: { clientId: 'cow' },
+ } );
+ const action = {
+ type: 'RESET_BLOCKS',
+ blocks: [
+ { clientId: 'another-block' },
+ { clientId: 'this-is-a-block-too' },
+ ],
+ };
+ const state = selection( original, action );
- expect( state1 ).toEqual( original );
- expect( state2 ).toEqual( original );
+ expect( state.selectionStart ).toEqual( {} );
+ expect( state.selectionEnd ).toEqual( {} );
} );
} );
diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js
index a0a7ba2f1fb37..964b105047082 100644
--- a/packages/block-editor/src/store/test/selectors.js
+++ b/packages/block-editor/src/store/test/selectors.js
@@ -807,8 +807,10 @@ describe( 'selectors', () => {
describe( 'hasSelectedBlock', () => {
it( 'should return false if no selection', () => {
const state = {
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( hasSelectedBlock( state ) ).toBe( false );
@@ -816,11 +818,13 @@ describe( 'selectors', () => {
it( 'should return false if multi-selection', () => {
const state = {
- selectionStart: {
- clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
- },
- selectionEnd: {
- clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189',
+ selection: {
+ selectionStart: {
+ clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
+ },
+ selectionEnd: {
+ clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189',
+ },
},
};
@@ -829,11 +833,13 @@ describe( 'selectors', () => {
it( 'should return true if singular selection', () => {
const state = {
- selectionStart: {
- clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
- },
- selectionEnd: {
- clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
+ selection: {
+ selectionStart: {
+ clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
+ },
+ selectionEnd: {
+ clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
+ },
},
};
@@ -891,8 +897,10 @@ describe( 'selectors', () => {
describe( 'getSelectedBlockClientId', () => {
it( 'should return null if no block is selected', () => {
const state = {
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( getSelectedBlockClientId( state ) ).toBe( null );
@@ -900,8 +908,10 @@ describe( 'selectors', () => {
it( 'should return null if there is multi selection', () => {
const state = {
- selectionStart: { clientId: 23 },
- selectionEnd: { clientId: 123 },
+ selection: {
+ selectionStart: { clientId: 23 },
+ selectionEnd: { clientId: 123 },
+ },
};
expect( getSelectedBlockClientId( state ) ).toBe( null );
@@ -916,8 +926,10 @@ describe( 'selectors', () => {
},
},
},
- selectionStart: { clientId: 23 },
- selectionEnd: { clientId: 23 },
+ selection: {
+ selectionStart: { clientId: 23 },
+ selectionEnd: { clientId: 23 },
+ },
};
expect( getSelectedBlockClientId( state ) ).toEqual( 23 );
@@ -946,8 +958,10 @@ describe( 'selectors', () => {
123: '',
},
},
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( getSelectedBlock( state ) ).toBe( null );
@@ -974,8 +988,10 @@ describe( 'selectors', () => {
23: '',
},
},
- selectionStart: { clientId: 23 },
- selectionEnd: { clientId: 123 },
+ selection: {
+ selectionStart: { clientId: 23 },
+ selectionEnd: { clientId: 123 },
+ },
};
expect( getSelectedBlock( state ) ).toBe( null );
@@ -1006,8 +1022,10 @@ describe( 'selectors', () => {
},
controlledInnerBlocks: {},
},
- selectionStart: { clientId: 23 },
- selectionEnd: { clientId: 23 },
+ selection: {
+ selectionStart: { clientId: 23 },
+ selectionEnd: { clientId: 23 },
+ },
};
expect( getSelectedBlock( state ) ).toEqual( {
@@ -1116,8 +1134,10 @@ describe( 'selectors', () => {
23: '',
},
},
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( getSelectedBlockClientIds( state ) ).toEqual( [] );
@@ -1137,8 +1157,10 @@ describe( 'selectors', () => {
5: '',
},
},
- selectionStart: { clientId: 2 },
- selectionEnd: { clientId: 2 },
+ selection: {
+ selectionStart: { clientId: 2 },
+ selectionEnd: { clientId: 2 },
+ },
};
expect( getSelectedBlockClientIds( state ) ).toEqual( [ 2 ] );
@@ -1158,8 +1180,10 @@ describe( 'selectors', () => {
5: '',
},
},
- selectionStart: { clientId: 2 },
- selectionEnd: { clientId: 4 },
+ selection: {
+ selectionStart: { clientId: 2 },
+ selectionEnd: { clientId: 4 },
+ },
};
expect( getSelectedBlockClientIds( state ) ).toEqual( [ 4, 3, 2 ] );
@@ -1184,8 +1208,10 @@ describe( 'selectors', () => {
9: 4,
},
},
- selectionStart: { clientId: 7 },
- selectionEnd: { clientId: 9 },
+ selection: {
+ selectionStart: { clientId: 7 },
+ selectionEnd: { clientId: 9 },
+ },
};
expect( getSelectedBlockClientIds( state ) ).toEqual( [ 9, 8, 7 ] );
@@ -1204,8 +1230,10 @@ describe( 'selectors', () => {
123: '',
},
},
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [] );
@@ -1225,8 +1253,10 @@ describe( 'selectors', () => {
5: '',
},
},
- selectionStart: { clientId: 2 },
- selectionEnd: { clientId: 4 },
+ selection: {
+ selectionStart: { clientId: 2 },
+ selectionEnd: { clientId: 4 },
+ },
};
expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [
@@ -1255,8 +1285,10 @@ describe( 'selectors', () => {
9: 4,
},
},
- selectionStart: { clientId: 7 },
- selectionEnd: { clientId: 9 },
+ selection: {
+ selectionStart: { clientId: 7 },
+ selectionEnd: { clientId: 9 },
+ },
};
expect( getMultiSelectedBlockClientIds( state ) ).toEqual( [
@@ -1276,8 +1308,10 @@ describe( 'selectors', () => {
order: {},
parents: {},
},
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( getMultiSelectedBlocks( state ) ).toBe(
@@ -1289,8 +1323,10 @@ describe( 'selectors', () => {
describe( 'getMultiSelectedBlocksStartClientId', () => {
it( 'returns null if there is no multi selection', () => {
const state = {
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( getMultiSelectedBlocksStartClientId( state ) ).toBeNull();
@@ -1298,8 +1334,10 @@ describe( 'selectors', () => {
it( 'returns multi selection start', () => {
const state = {
- selectionStart: { clientId: 2 },
- selectionEnd: { clientId: 4 },
+ selection: {
+ selectionStart: { clientId: 2 },
+ selectionEnd: { clientId: 4 },
+ },
};
expect( getMultiSelectedBlocksStartClientId( state ) ).toBe( 2 );
@@ -1309,8 +1347,10 @@ describe( 'selectors', () => {
describe( 'getMultiSelectedBlocksEndClientId', () => {
it( 'returns null if there is no multi selection', () => {
const state = {
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( getMultiSelectedBlocksEndClientId( state ) ).toBeNull();
@@ -1318,8 +1358,10 @@ describe( 'selectors', () => {
it( 'returns multi selection end', () => {
const state = {
- selectionStart: { clientId: 2 },
- selectionEnd: { clientId: 4 },
+ selection: {
+ selectionStart: { clientId: 2 },
+ selectionEnd: { clientId: 4 },
+ },
};
expect( getMultiSelectedBlocksEndClientId( state ) ).toBe( 4 );
@@ -1548,8 +1590,10 @@ describe( 'selectors', () => {
describe( 'isBlockSelected', () => {
it( 'should return true if the block is selected', () => {
const state = {
- selectionStart: { clientId: 123 },
- selectionEnd: { clientId: 123 },
+ selection: {
+ selectionStart: { clientId: 123 },
+ selectionEnd: { clientId: 123 },
+ },
};
expect( isBlockSelected( state, 123 ) ).toBe( true );
@@ -1557,8 +1601,10 @@ describe( 'selectors', () => {
it( 'should return false if a multi-selection range exists', () => {
const state = {
- selectionStart: { clientId: 123 },
- selectionEnd: { clientId: 124 },
+ selection: {
+ selectionStart: { clientId: 123 },
+ selectionEnd: { clientId: 124 },
+ },
};
expect( isBlockSelected( state, 123 ) ).toBe( false );
@@ -1566,8 +1612,10 @@ describe( 'selectors', () => {
it( 'should return false if the block is not selected', () => {
const state = {
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( isBlockSelected( state, 23 ) ).toBe( false );
@@ -1577,8 +1625,10 @@ describe( 'selectors', () => {
describe( 'hasSelectedInnerBlock', () => {
it( 'should return false if the selected block is a child of the given ClientId', () => {
const state = {
- selectionStart: { clientId: 5 },
- selectionEnd: { clientId: 5 },
+ selection: {
+ selectionStart: { clientId: 5 },
+ selectionEnd: { clientId: 5 },
+ },
blocks: {
order: {
4: [ 3, 2, 1 ],
@@ -1596,8 +1646,10 @@ describe( 'selectors', () => {
it( 'should return true if the selected block is a child of the given ClientId', () => {
const state = {
- selectionStart: { clientId: 3 },
- selectionEnd: { clientId: 3 },
+ selection: {
+ selectionStart: { clientId: 3 },
+ selectionEnd: { clientId: 3 },
+ },
blocks: {
order: {
4: [ 3, 2, 1 ],
@@ -1627,8 +1679,10 @@ describe( 'selectors', () => {
5: 6,
},
},
- selectionStart: { clientId: 2 },
- selectionEnd: { clientId: 4 },
+ selection: {
+ selectionStart: { clientId: 2 },
+ selectionEnd: { clientId: 4 },
+ },
};
expect( hasSelectedInnerBlock( state, 6 ) ).toBe( true );
} );
@@ -1647,8 +1701,10 @@ describe( 'selectors', () => {
5: 6,
},
},
- selectionStart: { clientId: 5 },
- selectionEnd: { clientId: 4 },
+ selection: {
+ selectionStart: { clientId: 5 },
+ selectionEnd: { clientId: 4 },
+ },
};
expect( hasSelectedInnerBlock( state, 3 ) ).toBe( false );
} );
@@ -1657,8 +1713,10 @@ describe( 'selectors', () => {
describe( 'isBlockWithinSelection', () => {
it( 'should return true if the block is selected but not the last', () => {
const state = {
- selectionStart: { clientId: 5 },
- selectionEnd: { clientId: 3 },
+ selection: {
+ selectionStart: { clientId: 5 },
+ selectionEnd: { clientId: 3 },
+ },
blocks: {
order: {
'': [ 5, 4, 3, 2, 1 ],
@@ -1678,8 +1736,10 @@ describe( 'selectors', () => {
it( 'should return false if the block is the last selected', () => {
const state = {
- selectionStart: { clientId: 5 },
- selectionEnd: { clientId: 3 },
+ selection: {
+ selectionStart: { clientId: 5 },
+ selectionEnd: { clientId: 3 },
+ },
blocks: {
order: {
'': [ 5, 4, 3, 2, 1 ],
@@ -1699,8 +1759,10 @@ describe( 'selectors', () => {
it( 'should return false if the block is not selected', () => {
const state = {
- selectionStart: { clientId: 5 },
- selectionEnd: { clientId: 3 },
+ selection: {
+ selectionStart: { clientId: 5 },
+ selectionEnd: { clientId: 3 },
+ },
blocks: {
order: {
'': [ 5, 4, 3, 2, 1 ],
@@ -1720,8 +1782,10 @@ describe( 'selectors', () => {
it( 'should return false if there is no selection', () => {
const state = {
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
blocks: {
order: {
'': [ 5, 4, 3, 2, 1 ],
@@ -1743,8 +1807,10 @@ describe( 'selectors', () => {
describe( 'hasMultiSelection', () => {
it( 'should return false if no selection', () => {
const state = {
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect( hasMultiSelection( state ) ).toBe( false );
@@ -1752,11 +1818,13 @@ describe( 'selectors', () => {
it( 'should return false if singular selection', () => {
const state = {
- selectionStart: {
- clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
- },
- selectionEnd: {
- clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
+ selection: {
+ selectionStart: {
+ clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
+ },
+ selectionEnd: {
+ clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
+ },
},
};
@@ -1765,11 +1833,13 @@ describe( 'selectors', () => {
it( 'should return true if multi-selection', () => {
const state = {
- selectionStart: {
- clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
- },
- selectionEnd: {
- clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189',
+ selection: {
+ selectionStart: {
+ clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1',
+ },
+ selectionEnd: {
+ clientId: '9db792c6-a25a-495d-adbd-97d56a4c4189',
+ },
},
};
@@ -1791,8 +1861,10 @@ describe( 'selectors', () => {
5: '',
},
},
- selectionStart: { clientId: 2 },
- selectionEnd: { clientId: 4 },
+ selection: {
+ selectionStart: { clientId: 2 },
+ selectionEnd: { clientId: 4 },
+ },
};
it( 'should return true if the block is multi selected', () => {
@@ -1818,8 +1890,10 @@ describe( 'selectors', () => {
5: '',
},
},
- selectionStart: { clientId: 2 },
- selectionEnd: { clientId: 4 },
+ selection: {
+ selectionStart: { clientId: 2 },
+ selectionEnd: { clientId: 4 },
+ },
};
it( 'should return true if the block is first in multi selection', () => {
@@ -1998,8 +2072,10 @@ describe( 'selectors', () => {
describe( 'getBlockInsertionPoint', () => {
it( 'should return the explicitly assigned insertion point', () => {
const state = {
- selectionStart: { clientId: 'clientId2' },
- selectionEnd: { clientId: 'clientId2' },
+ selection: {
+ selectionStart: { clientId: 'clientId2' },
+ selectionEnd: { clientId: 'clientId2' },
+ },
blocks: {
byClientId: {
clientId1: { clientId: 'clientId1' },
@@ -2033,8 +2109,10 @@ describe( 'selectors', () => {
it( 'should return an object for the selected block', () => {
const state = {
- selectionStart: { clientId: 'clientId1' },
- selectionEnd: { clientId: 'clientId1' },
+ selection: {
+ selectionStart: { clientId: 'clientId1' },
+ selectionEnd: { clientId: 'clientId1' },
+ },
blocks: {
byClientId: {
clientId1: { clientId: 'clientId1' },
@@ -2061,8 +2139,10 @@ describe( 'selectors', () => {
it( 'should return an object for the nested selected block', () => {
const state = {
- selectionStart: { clientId: 'clientId2' },
- selectionEnd: { clientId: 'clientId2' },
+ selection: {
+ selectionStart: { clientId: 'clientId2' },
+ selectionEnd: { clientId: 'clientId2' },
+ },
blocks: {
byClientId: {
clientId1: { clientId: 'clientId1' },
@@ -2093,8 +2173,10 @@ describe( 'selectors', () => {
it( 'should return an object for the last multi selected clientId', () => {
const state = {
- selectionStart: { clientId: 'clientId1' },
- selectionEnd: { clientId: 'clientId2' },
+ selection: {
+ selectionStart: { clientId: 'clientId1' },
+ selectionEnd: { clientId: 'clientId2' },
+ },
blocks: {
byClientId: {
clientId1: { clientId: 'clientId1' },
@@ -2125,8 +2207,10 @@ describe( 'selectors', () => {
it( 'should return an object for the last block if no selection', () => {
const state = {
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
blocks: {
byClientId: {
clientId1: { clientId: 'clientId1' },
@@ -3106,8 +3190,10 @@ describe( 'selectors', () => {
it( 'should not be defined if there is no block selected', () => {
const state = {
blocks,
- selectionStart: {},
- selectionEnd: {},
+ selection: {
+ selectionStart: {},
+ selectionEnd: {},
+ },
};
expect(
@@ -3118,8 +3204,10 @@ describe( 'selectors', () => {
it( 'should not be defined if selected block has no parent', () => {
const state = {
blocks,
- selectionStart: { clientId: 'b' },
- selectionEnd: { clientId: 'b' },
+ selection: {
+ selectionStart: { clientId: 'b' },
+ selectionEnd: { clientId: 'b' },
+ },
};
expect(
@@ -3130,8 +3218,10 @@ describe( 'selectors', () => {
it( 'should not be defined if selected block has no common parent with given block', () => {
const state = {
blocks,
- selectionStart: { clientId: 'd' },
- selectionEnd: { clientId: 'd' },
+ selection: {
+ selectionStart: { clientId: 'd' },
+ selectionEnd: { clientId: 'd' },
+ },
};
expect(
@@ -3142,8 +3232,10 @@ describe( 'selectors', () => {
it( 'should return block id if selected block is ancestor of given block', () => {
const state = {
blocks,
- selectionStart: { clientId: 'c' },
- selectionEnd: { clientId: 'c' },
+ selection: {
+ selectionStart: { clientId: 'c' },
+ selectionEnd: { clientId: 'c' },
+ },
};
expect(
@@ -3154,8 +3246,10 @@ describe( 'selectors', () => {
it( 'should return block id if selected block is nested child of given block', () => {
const state = {
blocks,
- selectionStart: { clientId: 'e' },
- selectionEnd: { clientId: 'e' },
+ selection: {
+ selectionStart: { clientId: 'e' },
+ selectionEnd: { clientId: 'e' },
+ },
};
expect(
@@ -3166,8 +3260,10 @@ describe( 'selectors', () => {
it( 'should return block id if selected block has common parent with given block', () => {
const state = {
blocks,
- selectionStart: { clientId: 'e' },
- selectionEnd: { clientId: 'e' },
+ selection: {
+ selectionStart: { clientId: 'e' },
+ selectionEnd: { clientId: 'e' },
+ },
};
expect(
@@ -3178,11 +3274,13 @@ describe( 'selectors', () => {
describe( 'getActiveBlockIdByBlockName', () => {
const state = {
- selectionStart: {
- clientId: 'client-id-04',
- },
- selectionEnd: {
- clientId: 'client-id-04',
+ selection: {
+ selectionStart: {
+ clientId: 'client-id-04',
+ },
+ selectionEnd: {
+ clientId: 'client-id-04',
+ },
},
blocks: {
parents: {
@@ -3242,7 +3340,7 @@ describe( 'selectors', () => {
).toEqual( 'client-id-02' );
} );
it( 'Should return first active matching block with (excluding self) when multi selected', () => {
- state.selectionEnd.clientId = 'client-id-05';
+ state.selection.selectionEnd.clientId = 'client-id-05';
expect(
getActiveBlockIdByBlockNames( state, [
diff --git a/packages/block-library/package.json b/packages/block-library/package.json
index b3cb76e28c861..ebe3030f55ce5 100644
--- a/packages/block-library/package.json
+++ b/packages/block-library/package.json
@@ -61,7 +61,6 @@
"memize": "^1.1.0",
"moment": "^2.22.1",
"react-easy-crop": "^3.0.0",
- "reakit": "1.3.4",
"tinycolor2": "^1.4.1"
},
"publishConfig": {
diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js
index 6e3f999ba3638..e28290836a0e8 100644
--- a/packages/block-library/src/block/edit.js
+++ b/packages/block-library/src/block/edit.js
@@ -22,6 +22,7 @@ import {
BlockControls,
InspectorControls,
useBlockProps,
+ Warning,
} from '@wordpress/block-editor';
import { store as reusableBlocksStore } from '@wordpress/reusable-blocks';
@@ -30,18 +31,25 @@ import { store as reusableBlocksStore } from '@wordpress/reusable-blocks';
*/
export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) {
- const recordArgs = [ 'postType', 'wp_block', ref ];
-
- const { reusableBlock, hasResolved } = useSelect(
- ( select ) => ( {
- reusableBlock: select( coreStore ).getEditedEntityRecord(
- ...recordArgs
- ),
- hasResolved: select( coreStore ).hasFinishedResolution(
- 'getEditedEntityRecord',
- recordArgs
- ),
- } ),
+ const { isMissing, hasResolved } = useSelect(
+ ( select ) => {
+ const persistedBlock = select( coreStore ).getEntityRecord(
+ 'postType',
+ 'wp_block',
+ ref
+ );
+ const hasResolvedBlock = select(
+ coreStore
+ ).hasFinishedResolution( 'getEntityRecord', [
+ 'postType',
+ 'wp_block',
+ ref,
+ ] );
+ return {
+ hasResolved: hasResolvedBlock,
+ isMissing: hasResolvedBlock && ! persistedBlock,
+ };
+ },
[ ref, clientId ]
);
@@ -75,21 +83,21 @@ export default function ReusableBlockEdit( { attributes: { ref }, clientId } ) {
const blockProps = useBlockProps();
- if ( ! hasResolved ) {
+ if ( isMissing ) {
return (
-
-
-
+
+ { __( 'Block has been deleted or is unavailable.' ) }
+
);
}
- if ( ! reusableBlock ) {
+ if ( ! hasResolved ) {
return (
- { __( 'Block has been deleted or is unavailable.' ) }
+
);
diff --git a/packages/block-library/src/buttons/content-justification-dropdown.js b/packages/block-library/src/buttons/content-justification-dropdown.js
index 7437a8670941e..40ecdd66f9464 100644
--- a/packages/block-library/src/buttons/content-justification-dropdown.js
+++ b/packages/block-library/src/buttons/content-justification-dropdown.js
@@ -3,29 +3,21 @@
*/
import { DropdownMenu } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-
-/**
- * Internal dependencies
- */
-import {
- contentJustificationCenterIcon,
- contentJustificationLeftIcon,
- contentJustificationRightIcon,
-} from './icons';
+import { justifyLeft, justifyCenter, justifyRight } from '@wordpress/icons';
const DEFAULT_ALLOWED_VALUES = [ 'left', 'center', 'right' ];
const CONTROLS = {
left: {
- icon: contentJustificationLeftIcon,
+ icon: justifyLeft,
title: __( 'Justify content left' ),
},
center: {
- icon: contentJustificationCenterIcon,
+ icon: justifyCenter,
title: __( 'Justify content center' ),
},
right: {
- icon: contentJustificationRightIcon,
+ icon: justifyRight,
title: __( 'Justify content right' ),
},
};
diff --git a/packages/block-library/src/buttons/icons.js b/packages/block-library/src/buttons/icons.js
deleted file mode 100644
index 38378b3afaa32..0000000000000
--- a/packages/block-library/src/buttons/icons.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { Path, SVG } from '@wordpress/components';
-
-export const contentJustificationLeftIcon = (
-
-);
-
-export const contentJustificationCenterIcon = (
-
-);
-
-export const contentJustificationRightIcon = (
-
-);
diff --git a/packages/block-library/src/cover/deprecated.js b/packages/block-library/src/cover/deprecated.js
index 44effb065bfbf..fe08fb9cd72a8 100644
--- a/packages/block-library/src/cover/deprecated.js
+++ b/packages/block-library/src/cover/deprecated.js
@@ -24,6 +24,8 @@ import {
VIDEO_BACKGROUND_TYPE,
backgroundImageStyles,
dimRatioToClass,
+ getPositionClassName,
+ isContentPositionCenter,
} from './shared';
const blockAttributes = {
@@ -57,6 +59,134 @@ const blockAttributes = {
};
const deprecated = [
+ {
+ attributes: {
+ ...blockAttributes,
+ title: {
+ type: 'string',
+ source: 'html',
+ selector: 'p',
+ },
+ contentAlign: {
+ type: 'string',
+ default: 'center',
+ },
+ minHeight: {
+ type: 'number',
+ },
+ gradient: {
+ type: 'string',
+ },
+ customGradient: {
+ type: 'string',
+ },
+ },
+ save( { attributes } ) {
+ const {
+ backgroundType,
+ gradient,
+ contentPosition,
+ customGradient,
+ customOverlayColor,
+ dimRatio,
+ focalPoint,
+ hasParallax,
+ overlayColor,
+ url,
+ minHeight: minHeightProp,
+ minHeightUnit,
+ } = attributes;
+ const overlayColorClass = getColorClassName(
+ 'background-color',
+ overlayColor
+ );
+ const gradientClass = __experimentalGetGradientClass( gradient );
+ const minHeight = minHeightUnit
+ ? `${ minHeightProp }${ minHeightUnit }`
+ : minHeightProp;
+
+ const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;
+ const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType;
+
+ const style = isImageBackground ? backgroundImageStyles( url ) : {};
+ const videoStyle = {};
+
+ if ( ! overlayColorClass ) {
+ style.backgroundColor = customOverlayColor;
+ }
+
+ if ( customGradient && ! url ) {
+ style.background = customGradient;
+ }
+ style.minHeight = minHeight || undefined;
+
+ let positionValue;
+
+ if ( focalPoint ) {
+ positionValue = `${ Math.round(
+ focalPoint.x * 100
+ ) }% ${ Math.round( focalPoint.y * 100 ) }%`;
+
+ if ( isImageBackground && ! hasParallax ) {
+ style.backgroundPosition = positionValue;
+ }
+
+ if ( isVideoBackground ) {
+ videoStyle.objectPosition = positionValue;
+ }
+ }
+
+ const classes = classnames(
+ dimRatioToClass( dimRatio ),
+ overlayColorClass,
+ {
+ 'has-background-dim': dimRatio !== 0,
+ 'has-parallax': hasParallax,
+ 'has-background-gradient': gradient || customGradient,
+ [ gradientClass ]: ! url && gradientClass,
+ 'has-custom-content-position': ! isContentPositionCenter(
+ contentPosition
+ ),
+ },
+ getPositionClassName( contentPosition )
+ );
+
+ return (
+
+ { url &&
+ ( gradient || customGradient ) &&
+ dimRatio !== 0 && (
+
+ ) }
+ { isVideoBackground && url && (
+
+ ) }
+
+
+
+
+ );
+ },
+ },
{
attributes: {
...blockAttributes,
diff --git a/packages/block-library/src/cover/edit.js b/packages/block-library/src/cover/edit.js
index 00ea5c4663320..d9b685ed300aa 100644
--- a/packages/block-library/src/cover/edit.js
+++ b/packages/block-library/src/cover/edit.js
@@ -331,23 +331,21 @@ function CoverEdit( {
: minHeight;
const style = {
- ...( isImageBackground ? backgroundImageStyles( url ) : {} ),
+ ...( isImageBackground && hasParallax
+ ? backgroundImageStyles( url )
+ : {} ),
backgroundColor: overlayColor.color,
+ background: gradientValue && ! url ? gradientValue : undefined,
minHeight: temporaryMinHeight || minHeightWithUnit || undefined,
};
- if ( gradientValue && ! url ) {
- style.background = gradientValue;
- }
-
- let positionValue;
-
- if ( focalPoint ) {
- positionValue = `${ focalPoint.x * 100 }% ${ focalPoint.y * 100 }%`;
- if ( isImageBackground ) {
- style.backgroundPosition = positionValue;
- }
- }
+ const mediaStyle = {
+ objectPosition:
+ // prettier-ignore
+ focalPoint && ! hasParallax
+ ? `${ Math.round( focalPoint.x * 100 ) }% ${ Math.round( focalPoint.y * 100) }%`
+ : undefined,
+ };
const hasBackground = !! ( url || overlayColor.color || gradientValue );
const showFocalPointPicker =
@@ -587,18 +585,6 @@ function CoverEdit( {
} }
showHandle={ isSelected }
/>
- { isImageBackground && (
- // Used only to programmatically check if the image is dark or not
-
- ) }
{ url && gradientValue && dimRatio !== 0 && (
) }
+ { isImageBackground && ! hasParallax && (
+
+ ) }
{ isVideoBackground && (
) }
{ isBlogUrl && }
diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss
index 9808a20dadeaa..3d4e5c717d894 100644
--- a/packages/block-library/src/cover/editor.scss
+++ b/packages/block-library/src/cover/editor.scss
@@ -43,6 +43,12 @@
.wp-block-cover__placeholder-background-options {
width: 100%;
}
+
+ // Fix object-fit when block is full width in the editor.
+ .wp-block-cover__image-background {
+ width: inherit;
+ height: inherit;
+ }
}
[data-align="left"] > .wp-block-cover,
diff --git a/packages/block-library/src/cover/save.js b/packages/block-library/src/cover/save.js
index cc9f1d44ab84a..0297dd30ecdf2 100644
--- a/packages/block-library/src/cover/save.js
+++ b/packages/block-library/src/cover/save.js
@@ -38,6 +38,7 @@ export default function save( { attributes } ) {
isRepeated,
overlayColor,
url,
+ id,
minHeight: minHeightProp,
minHeightUnit,
} = attributes;
@@ -53,33 +54,20 @@ export default function save( { attributes } ) {
const isImageBackground = IMAGE_BACKGROUND_TYPE === backgroundType;
const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType;
- const style = isImageBackground ? backgroundImageStyles( url ) : {};
- const videoStyle = {};
+ const style = {
+ ...( isImageBackground && hasParallax
+ ? backgroundImageStyles( url )
+ : {} ),
+ backgroundColor: ! overlayColorClass ? customOverlayColor : undefined,
+ background: customGradient && ! url ? customGradient : undefined,
+ minHeight: minHeight || undefined,
+ };
- if ( ! overlayColorClass ) {
- style.backgroundColor = customOverlayColor;
- }
-
- if ( customGradient && ! url ) {
- style.background = customGradient;
- }
- style.minHeight = minHeight || undefined;
-
- let positionValue;
-
- if ( focalPoint ) {
- positionValue = `${ Math.round( focalPoint.x * 100 ) }% ${ Math.round(
- focalPoint.y * 100
- ) }%`;
-
- if ( isImageBackground && ! hasParallax ) {
- style.backgroundPosition = positionValue;
- }
-
- if ( isVideoBackground ) {
- videoStyle.objectPosition = positionValue;
- }
- }
+ const objectPosition =
+ // prettier-ignore
+ focalPoint && ! hasParallax
+ ? `${ Math.round( focalPoint.x * 100 ) }% ${ Math.round( focalPoint.y * 100 ) }%`
+ : undefined;
const classes = classnames(
dimRatioToClass( dimRatio ),
@@ -113,15 +101,33 @@ export default function save( { attributes } ) {
}
/>
) }
+ { isImageBackground && url && ! hasParallax && (
+
+ ) }
{ isVideoBackground && url && (
) }
diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss
index 1f4f09f9fc9c1..b4b34e52c7d5a 100644
--- a/packages/block-library/src/cover/style.scss
+++ b/packages/block-library/src/cover/style.scss
@@ -4,7 +4,6 @@
background-size: cover;
background-position: center center;
min-height: 430px;
- height: 100%;
width: 100%;
display: flex;
justify-content: center;
@@ -169,17 +168,22 @@
}
}
+.wp-block-cover__image-background,
.wp-block-cover__video-background {
position: absolute;
- top: 50%;
- left: 50%;
- transform: translateX(-50%) translateY(-50%);
width: 100%;
height: 100%;
- z-index: z-index(".wp-block-cover__video-background");
object-fit: cover;
}
+.wp-block-cover__video-background {
+ z-index: z-index(".wp-block-cover__video-background");
+}
+
+.wp-block-cover__image-background {
+ z-index: z-index(".wp-block-cover__image-background");
+}
+
// Styles bellow only exist to support older versions of the block.
// Versions that not had inner blocks and used an h2 heading had a section (and not a div) with a class wp-block-cover-image (and not a wp-block-cover).
// We are using the previous referred differences to target old versions.
diff --git a/packages/block-library/src/cover/transforms.js b/packages/block-library/src/cover/transforms.js
index 57de4c49acc88..85b340af2e9bb 100644
--- a/packages/block-library/src/cover/transforms.js
+++ b/packages/block-library/src/cover/transforms.js
@@ -14,26 +14,42 @@ const transforms = {
type: 'block',
blocks: [ 'core/image' ],
transform: ( { caption, url, align, id, anchor } ) =>
- createBlock( 'core/cover', {
- title: caption,
- url,
- align,
- id,
- anchor,
- } ),
+ createBlock(
+ 'core/cover',
+ {
+ url,
+ align,
+ id,
+ anchor,
+ },
+ [
+ createBlock( 'core/paragraph', {
+ content: caption,
+ fontSize: 'large',
+ } ),
+ ]
+ ),
},
{
type: 'block',
blocks: [ 'core/video' ],
transform: ( { caption, src, align, id, anchor } ) =>
- createBlock( 'core/cover', {
- title: caption,
- url: src,
- align,
- id,
- backgroundType: VIDEO_BACKGROUND_TYPE,
- anchor,
- } ),
+ createBlock(
+ 'core/cover',
+ {
+ url: src,
+ align,
+ id,
+ backgroundType: VIDEO_BACKGROUND_TYPE,
+ anchor,
+ },
+ [
+ createBlock( 'core/paragraph', {
+ content: caption,
+ fontSize: 'large',
+ } ),
+ ]
+ ),
},
],
to: [
diff --git a/packages/block-library/src/embed/transforms.js b/packages/block-library/src/embed/transforms.js
index 1709d0ffc0893..c2db0d435b116 100644
--- a/packages/block-library/src/embed/transforms.js
+++ b/packages/block-library/src/embed/transforms.js
@@ -20,7 +20,8 @@ const transforms = {
type: 'raw',
isMatch: ( node ) =>
node.nodeName === 'P' &&
- /^\s*(https?:\/\/\S+)\s*$/i.test( node.textContent ),
+ /^\s*(https?:\/\/\S+)\s*$/i.test( node.textContent ) &&
+ node.textContent?.match( /https/gi )?.length === 1,
transform: ( node ) => {
return createBlock( EMBED_BLOCK, {
url: node.textContent.trim(),
diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss
index c67ce5bf3778d..1b047b4e2b8f6 100644
--- a/packages/block-library/src/gallery/editor.scss
+++ b/packages/block-library/src/gallery/editor.scss
@@ -38,12 +38,18 @@ figure.wp-block-gallery {
}
figure.is-selected {
- box-shadow: 0 0 0 $border-width $white, 0 0 0 3px var(--wp-admin-theme-color);
- border-radius: $radius-block-ui;
- outline: 2px solid transparent;
- img {
- border-radius: $radius-block-ui;
+ &::before {
+ box-shadow: 0 0 0 $border-width $white inset, 0 0 0 3px var(--wp-admin-theme-color) inset;
+ content: "";
+ // Shown in Windows 10 high contrast mode.
+ outline: 2px solid transparent;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
}
}
diff --git a/packages/block-library/src/list/edit.js b/packages/block-library/src/list/edit.js
index b653e39e3dfb5..afb5d562e33eb 100644
--- a/packages/block-library/src/list/edit.js
+++ b/packages/block-library/src/list/edit.js
@@ -91,7 +91,8 @@ export default function ListEdit( {
icon: isRTL()
? formatListBulletsRTL
: formatListBullets,
- title: __( 'Convert to unordered list' ),
+ title: __( 'Unordered' ),
+ describedBy: __( 'Convert to unordered list' ),
isActive: isActiveListType( value, 'ul', tagName ),
onClick() {
onChange(
@@ -108,7 +109,8 @@ export default function ListEdit( {
icon: isRTL()
? formatListNumberedRTL
: formatListNumbered,
- title: __( 'Convert to ordered list' ),
+ title: __( 'Ordered' ),
+ describedBy: __( 'Convert to ordered list' ),
isActive: isActiveListType( value, 'ol', tagName ),
onClick() {
onChange(
@@ -123,7 +125,8 @@ export default function ListEdit( {
},
{
icon: isRTL() ? formatOutdentRTL : formatOutdent,
- title: __( 'Outdent list item' ),
+ title: __( 'Outdent' ),
+ describedBy: __( 'Outdent list item' ),
shortcut: _x( 'Backspace', 'keyboard key' ),
isDisabled: ! canOutdentListItems( value ),
onClick() {
@@ -133,7 +136,8 @@ export default function ListEdit( {
},
{
icon: isRTL() ? formatIndentRTL : formatIndent,
- title: __( 'Indent list item' ),
+ title: __( 'Indent' ),
+ describedBy: __( 'Indent list item' ),
shortcut: _x( 'Space', 'keyboard key' ),
isDisabled: ! canIndentListItems( value ),
onClick() {
diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js
index 4f43872339399..029def990d245 100644
--- a/packages/block-library/src/navigation-link/edit.js
+++ b/packages/block-library/src/navigation-link/edit.js
@@ -232,7 +232,7 @@ function NavigationLinkEdit( {
const page = await saveEntityRecord( 'postType', postType, {
title: pageTitle,
- status: 'publish',
+ status: 'draft',
} );
return {
@@ -389,12 +389,12 @@ function NavigationLinkEdit( {
if ( type === 'post' ) {
/* translators: %s: search term. */
format = __(
- 'Create post:
%s'
+ 'Create draft post:
%s'
);
} else {
/* translators: %s: search term. */
format = __(
- 'Create page:
%s'
+ 'Create draft page:
%s'
);
}
return createInterpolateElement(
diff --git a/packages/block-library/src/navigation-link/index.php b/packages/block-library/src/navigation-link/index.php
index 57c4b3354fc09..1513f3034514e 100644
--- a/packages/block-library/src/navigation-link/index.php
+++ b/packages/block-library/src/navigation-link/index.php
@@ -104,6 +104,14 @@ function block_core_navigation_link_render_submenu_icon() {
* @return string Returns the post content with the legacy widget added.
*/
function render_block_core_navigation_link( $attributes, $content, $block ) {
+ // Don't render the block's subtree if it is a draft.
+ if ( isset( $attributes['id'] ) && is_numeric( $attributes['id'] ) ) {
+ $post = get_post( $attributes['id'] );
+ if ( 'publish' !== $post->post_status ) {
+ return '';
+ }
+ }
+
// Don't render the block's subtree if it has no label.
if ( empty( $attributes['label'] ) ) {
return '';
diff --git a/packages/block-library/src/navigation/edit.js b/packages/block-library/src/navigation/edit.js
index cd47d5b89a0b9..c62d48908518a 100644
--- a/packages/block-library/src/navigation/edit.js
+++ b/packages/block-library/src/navigation/edit.js
@@ -1,7 +1,6 @@
/**
* External dependencies
*/
-import { upperFirst } from 'lodash';
import classnames from 'classnames';
/**
@@ -24,7 +23,12 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import useBlockNavigator from './use-block-navigator';
-import * as navIcons from './icons';
+import {
+ justifyLeft,
+ justifyCenter,
+ justifyRight,
+ justifySpaceBetween,
+} from '@wordpress/icons';
import NavigationPlaceholder from './placeholder';
function Navigation( {
@@ -38,9 +42,16 @@ function Navigation( {
updateInnerBlocks,
className,
hasSubmenuIndicatorSetting = true,
- hasItemJustificationControls = true,
+ hasItemJustificationControls = attributes.orientation === 'horizontal',
hasListViewModal = true,
} ) {
+ const navIcons = {
+ left: justifyLeft,
+ center: justifyCenter,
+ right: justifyRight,
+ 'space-between': justifySpaceBetween,
+ };
+
const [ isPlaceholderShown, setIsPlaceholderShown ] = useState(
! hasExistingNavItems
);
@@ -110,6 +121,11 @@ function Navigation( {
};
}
+ const POPOVER_PROPS = {
+ position: 'bottom right',
+ isAlternate: true,
+ };
+
return (
<>
@@ -117,37 +133,44 @@ function Navigation( {
) }
diff --git a/packages/block-library/src/navigation/icons.js b/packages/block-library/src/navigation/icons.js
deleted file mode 100644
index b18ef92f2a306..0000000000000
--- a/packages/block-library/src/navigation/icons.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { Path, SVG } from '@wordpress/components';
-
-export const justifyLeftIcon = (
-
-);
-
-export const justifyCenterIcon = (
-
-);
-
-export const justifyRightIcon = (
-
-);
diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss
index 842d84108e193..ddb065f68aca9 100644
--- a/packages/block-library/src/navigation/style.scss
+++ b/packages/block-library/src/navigation/style.scss
@@ -18,3 +18,7 @@
.items-justified-right > ul {
justify-content: flex-end;
}
+
+.items-justified-space-between > ul {
+ justify-content: space-between;
+}
diff --git a/packages/block-library/src/paragraph/style.scss b/packages/block-library/src/paragraph/style.scss
index 87761b95ee122..8a750a271cdd4 100644
--- a/packages/block-library/src/paragraph/style.scss
+++ b/packages/block-library/src/paragraph/style.scss
@@ -35,3 +35,8 @@ p.has-background {
p.has-text-color a {
color: inherit;
}
+
+// Prevent an empty P tag from collapsing, so it matches the backend.
+p:empty::before {
+ content: "\200B";
+}
diff --git a/packages/block-library/src/post-comments/index.php b/packages/block-library/src/post-comments/index.php
index d9629c32f0e6f..7f15d83984ee4 100644
--- a/packages/block-library/src/post-comments/index.php
+++ b/packages/block-library/src/post-comments/index.php
@@ -25,10 +25,13 @@ function render_block_core_post_comments( $attributes, $content, $block ) {
$post = get_post( $block->context['postId'] );
setup_postdata( $post );
- // This generates a deprecate message.
- // Ideally this deprecation is removed.
ob_start();
+ // There's a deprecation warning generated by WP Core.
+ // Ideally this deprecation is removed from Core.
+ // In the meantime, this removes it from the output.
+ add_filter( 'deprecated_file_trigger_error', '__return_false' );
comments_template();
+ remove_filter( 'deprecated_file_trigger_error', '__return_false' );
$post = $post_before;
$classes = '';
@@ -37,8 +40,9 @@ function render_block_core_post_comments( $attributes, $content, $block ) {
}
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $classes ) );
- $output = sprintf( '', $wrapper_attributes ) . ob_get_clean() . '
';
- return $output;
+ $output = ob_get_clean();
+
+ return sprintf( '%2$s
', $wrapper_attributes, $output );
}
/**
diff --git a/packages/block-library/src/query-loop/block.json b/packages/block-library/src/query-loop/block.json
index 6059febae1dd6..724169afc851d 100644
--- a/packages/block-library/src/query-loop/block.json
+++ b/packages/block-library/src/query-loop/block.json
@@ -6,7 +6,8 @@
"queryId",
"query",
"queryContext",
- "layout"
+ "layout",
+ "templateSlug"
],
"supports": {
"reusable": false,
diff --git a/packages/block-library/src/query-loop/edit.js b/packages/block-library/src/query-loop/edit.js
index 370eb45e08a7d..1606b913e260a 100644
--- a/packages/block-library/src/query-loop/edit.js
+++ b/packages/block-library/src/query-loop/edit.js
@@ -43,11 +43,12 @@ export default function QueryLoopEdit( {
sticky,
inherit,
} = {},
- queryContext,
+ queryContext = [ {} ],
+ templateSlug,
layout: { type: layoutType = 'flex', columns = 1 } = {},
},
} ) {
- const [ { page } ] = useQueryContext() || queryContext || [ {} ];
+ const [ { page } ] = useQueryContext() || queryContext;
const [ activeBlockContext, setActiveBlockContext ] = useState();
const { posts, blocks } = useSelect(
@@ -79,32 +80,14 @@ export default function QueryLoopEdit( {
if ( sticky ) {
query.sticky = sticky === 'only';
}
-
- // When you insert this block outside of the edit site then store
- // does not exist therefore we check for its existence.
- // TODO: remove this code, edit-site shouldn't be called in block-library.
- // This creates a cycle dependency.
- if ( inherit && select( 'core/edit-site' ) ) {
- // This should be passed from the context exposed by edit site.
- const { getEditedPostType, getEditedPostId } = select(
- 'core/edit-site'
- );
-
- if ( 'wp_template' === getEditedPostType() ) {
- const { slug } = select( 'core' ).getEntityRecord(
- 'postType',
- 'wp_template',
- getEditedPostId()
- );
-
- // Change the post-type if needed.
- if ( slug?.startsWith( 'archive-' ) ) {
- query.postType = slug.replace( 'archive-', '' );
- postType = query.postType;
- }
+ // If `inherit` is truthy, adjust conditionally the query to create a better preview.
+ if ( inherit ) {
+ // Change the post-type if needed.
+ if ( templateSlug?.startsWith( 'archive-' ) ) {
+ query.postType = templateSlug.replace( 'archive-', '' );
+ postType = query.postType;
}
}
-
return {
posts: getEntityRecords( 'postType', postType, query ),
blocks: getBlocks( clientId ),
@@ -125,6 +108,7 @@ export default function QueryLoopEdit( {
exclude,
sticky,
inherit,
+ templateSlug,
]
);
diff --git a/packages/block-library/src/query/edit/index.js b/packages/block-library/src/query/edit/index.js
index 45be4c7f88537..495c27f71012b 100644
--- a/packages/block-library/src/query/edit/index.js
+++ b/packages/block-library/src/query/edit/index.js
@@ -50,7 +50,7 @@ export function QueryContent( {
if ( !! Object.keys( newQuery ).length ) {
updateQuery( newQuery );
}
- }, [ query.perPage, query.exclude, postId ] );
+ }, [ query.perPage, query.exclude, query.inherit, postId ] );
// We need this for multi-query block pagination.
// Query parameters for each block are scoped to their ID.
useEffect( () => {
diff --git a/packages/block-library/src/spacer/editor.scss b/packages/block-library/src/spacer/editor.scss
index d5616f713e9f8..66aa8ef55bb06 100644
--- a/packages/block-library/src/spacer/editor.scss
+++ b/packages/block-library/src/spacer/editor.scss
@@ -10,8 +10,9 @@
}
}
+.wp-block-spacer.is-hovered .block-library-spacer__resize-container,
.block-library-spacer__resize-container.has-show-handle {
- background: $gray-100;
+ background: rgba($black, 0.1);
.is-dark-theme & {
background: rgba($white, 0.15);
diff --git a/packages/block-library/src/template-part/edit/selection/template-part-previews.js b/packages/block-library/src/template-part/edit/selection/template-part-previews.js
index 812ed35907e85..e3b7b3e669926 100644
--- a/packages/block-library/src/template-part/edit/selection/template-part-previews.js
+++ b/packages/block-library/src/template-part/edit/selection/template-part-previews.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import { groupBy, deburr } from 'lodash';
+
/**
* WordPress dependencies
*/
@@ -7,16 +12,15 @@ import { useMemo, useCallback } from '@wordpress/element';
import { ENTER, SPACE } from '@wordpress/keycodes';
import { __, sprintf } from '@wordpress/i18n';
import { BlockPreview } from '@wordpress/block-editor';
-import { Icon } from '@wordpress/components';
+import {
+ __unstableComposite as Composite,
+ __unstableCompositeItem as CompositeItem,
+ Icon,
+ __unstableUseCompositeState as useCompositeState,
+} from '@wordpress/components';
import { useAsyncList } from '@wordpress/compose';
import { store as noticesStore } from '@wordpress/notices';
-/**
- * External dependencies
- */
-import { groupBy, deburr } from 'lodash';
-import { Composite, useCompositeState, CompositeItem } from 'reakit';
-
function PreviewPlaceholder() {
return (
output[] = (array) self::freeform(
+ $this->output[] = (array) $this->freeform(
substr(
$this->document,
$leading_html_start,
@@ -492,7 +492,7 @@ function add_freeform( $length = null ) {
return;
}
- $this->output[] = (array) self::freeform( substr( $this->document, $this->offset, $length ) );
+ $this->output[] = (array) $this->freeform( substr( $this->document, $this->offset, $length ) );
}
/**
@@ -541,7 +541,7 @@ function add_block_from_stack( $end_offset = null ) {
}
if ( isset( $stack_top->leading_html_start ) ) {
- $this->output[] = (array) self::freeform(
+ $this->output[] = (array) $this->freeform(
substr(
$this->document,
$stack_top->leading_html_start,
diff --git a/packages/components/src/alignment-matrix-control/cell.js b/packages/components/src/alignment-matrix-control/cell.js
index 627d98aff2f9d..48dd339ebfc5a 100644
--- a/packages/components/src/alignment-matrix-control/cell.js
+++ b/packages/components/src/alignment-matrix-control/cell.js
@@ -1,11 +1,7 @@
-/**
- * External dependencies
- */
-import { CompositeItem } from 'reakit/Composite';
-
/**
* Internal dependencies
*/
+import { CompositeItem } from '../composite';
import Tooltip from '../tooltip';
import VisuallyHidden from '../visually-hidden';
diff --git a/packages/components/src/alignment-matrix-control/index.js b/packages/components/src/alignment-matrix-control/index.js
index b0c56b09e819c..d9665eb5d446d 100644
--- a/packages/components/src/alignment-matrix-control/index.js
+++ b/packages/components/src/alignment-matrix-control/index.js
@@ -3,7 +3,6 @@
*/
import { noop } from 'lodash';
import classnames from 'classnames';
-import { useCompositeState, Composite, CompositeGroup } from 'reakit';
/**
* WordPress dependencies
@@ -16,6 +15,7 @@ import { useState, useEffect } from '@wordpress/element';
* Internal dependencies
*/
import Cell from './cell';
+import { Composite, CompositeGroup, useCompositeState } from '../composite';
import { Root, Row } from './styles/alignment-matrix-control-styles';
import AlignmentMatrixControlIcon from './icon';
import { GRID, getItemId } from './utils';
diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js
index 698c52e453b8a..869f4279a9948 100644
--- a/packages/components/src/button/index.js
+++ b/packages/components/src/button/index.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
-import { isArray } from 'lodash';
+import { isArray, uniqueId } from 'lodash';
/**
* WordPress dependencies
@@ -15,6 +15,7 @@ import { forwardRef } from '@wordpress/element';
*/
import Tooltip from '../tooltip';
import Icon from '../icon';
+import VisuallyHidden from '../visually-hidden';
const disabledEventsOnDisabledButton = [ 'onMouseDown', 'onClick' ];
@@ -43,6 +44,7 @@ export function Button( props, ref ) {
children,
text,
__experimentalIsFocusable: isFocusable,
+ describedBy,
...additionalProps
} = props;
@@ -104,12 +106,18 @@ export function Button( props, ref ) {
// the tooltip is not explicitly disabled.
false !== showTooltip ) );
+ const descriptionId = describedBy ? uniqueId() : null;
+
+ const describedById =
+ additionalProps[ 'aria-describedby' ] || descriptionId;
+
const element = (
{ icon && iconPosition === 'left' && (
@@ -124,17 +132,33 @@ export function Button( props, ref ) {
);
if ( ! shouldShowTooltip ) {
- return element;
+ return (
+ <>
+ { element }
+ { describedBy && (
+
+ { describedBy }
+
+ ) }
+ >
+ );
}
return (
-
- { element }
-
+ <>
+
+ { element }
+
+ { describedBy && (
+
+ { describedBy }
+
+ ) }
+ >
);
}
diff --git a/packages/components/src/button/test/index.js b/packages/components/src/button/test/index.js
index 49de9ef1d59ad..de027bbe41fed 100644
--- a/packages/components/src/button/test/index.js
+++ b/packages/components/src/button/test/index.js
@@ -3,7 +3,6 @@
*/
import { shallow } from 'enzyme';
import TestUtils from 'react-dom/test-utils';
-
/**
* WordPress dependencies
*/
@@ -18,7 +17,7 @@ import ButtonWithForwardedRef, { Button } from '../';
describe( 'Button', () => {
describe( 'basic rendering', () => {
it( 'should render a button element with only one class', () => {
- const button = shallow( );
+ const button = shallow( ).find( 'button' );
expect( button.hasClass( 'components-button' ) ).toBe( true );
expect( button.hasClass( 'is-large' ) ).toBe( false );
expect( button.hasClass( 'is-primary' ) ).toBe( false );
@@ -30,13 +29,15 @@ describe( 'Button', () => {
} );
it( 'should render a button element with is-primary class', () => {
- const button = shallow( );
+ const button = shallow( ).find( 'button' );
expect( button.hasClass( 'is-large' ) ).toBe( false );
expect( button.hasClass( 'is-primary' ) ).toBe( true );
} );
it( 'should render a button element with is-small class', () => {
- const button = shallow( );
+ const button = shallow( ).find(
+ 'button'
+ );
expect( button.hasClass( 'is-secondary' ) ).toBe( true );
expect( button.hasClass( 'is-large' ) ).toBe( false );
expect( button.hasClass( 'is-small' ) ).toBe( true );
@@ -44,48 +45,58 @@ describe( 'Button', () => {
} );
it( 'should render a button element with is-pressed without button class', () => {
- const button = shallow( );
+ const button = shallow( ).find( 'button' );
expect( button.hasClass( 'is-pressed' ) ).toBe( true );
} );
it( 'should add a disabled prop to the button', () => {
- const button = shallow( );
+ const button = shallow( ).find( 'button' );
expect( button.prop( 'disabled' ) ).toBe( true );
} );
it( 'should add only aria-disabled attribute when disabled and isFocusable are true', () => {
const button = shallow(
- );
+ ).find( 'button' );
expect( button.prop( 'disabled' ) ).toBe( false );
expect( button.prop( 'aria-disabled' ) ).toBe( true );
} );
it( 'should not poss the prop target into the element', () => {
- const button = shallow( );
+ const button = shallow( ).find(
+ 'button'
+ );
expect( button.prop( 'target' ) ).toBeUndefined();
} );
it( 'should render with an additional className', () => {
- const button = shallow( );
+ const button = shallow( ).find(
+ 'button'
+ );
expect( button.hasClass( 'gutenberg' ) ).toBe( true );
} );
it( 'should render and additional WordPress prop of value awesome', () => {
- const button = shallow( );
+ const button = shallow( ).find(
+ 'button'
+ );
expect( button.prop( 'WordPress' ) ).toBe( 'awesome' );
} );
it( 'should render an icon button', () => {
- const iconButton = shallow( );
+ const iconButton = shallow( ).find(
+ 'button'
+ );
expect( iconButton.hasClass( 'has-icon' ) ).toBe( true );
expect( iconButton.prop( 'aria-label' ) ).toBeUndefined();
} );
it( 'should render a Dashicon component matching the wordpress icon', () => {
- const iconButton = shallow( );
+ const iconButton = shallow( ).find(
+ 'button'
+ );
expect( iconButton.find( 'Icon' ).dive() ).not.toBeNull();
} );
@@ -95,7 +106,7 @@ describe( 'Button', () => {
icon={ plusCircle }
children={ Test
}
/>
- );
+ ).find( 'button' );
expect( iconButton.find( 'Icon' ).dive() ).not.toBeNull();
expect( iconButton.find( '.test' ).shallow().text() ).toBe(
'Test'
@@ -105,7 +116,7 @@ describe( 'Button', () => {
it( 'should add an aria-label when the label property is used, with Tooltip wrapper', () => {
const iconButton = shallow(
- );
+ ).find( 'Tooltip' );
expect( iconButton.name() ).toBe( 'Tooltip' );
expect( iconButton.prop( 'text' ) ).toBe( 'WordPress' );
expect( iconButton.find( 'button' ).prop( 'aria-label' ) ).toBe(
@@ -114,10 +125,34 @@ describe( 'Button', () => {
} );
it( 'should support explicit aria-label override', () => {
- const iconButton = shallow( );
+ const iconButton = shallow( ).find(
+ 'button'
+ );
expect( iconButton.prop( 'aria-label' ) ).toBe( 'Custom' );
} );
+ it( 'should support adding aria-describedby text', () => {
+ const buttonDescription = shallow(
+
+ )
+ .find( 'VisuallyHidden' )
+ .shallow()
+ .text();
+ expect( buttonDescription ).toBe( 'Description text' );
+ } );
+
+ it( 'should populate tooltip with describedBy content', () => {
+ const buttonTooltip = shallow(
+
+ ).find( 'Tooltip' );
+
+ expect( buttonTooltip.prop( 'text' ) ).toBe( 'Description text' );
+ } );
+
it( 'should allow tooltip disable', () => {
const iconButton = shallow(
{
label="WordPress"
showTooltip={ false }
/>
- );
+ ).find( 'button' );
expect( iconButton.name() ).toBe( 'button' );
expect( iconButton.prop( 'aria-label' ) ).toBe( 'WordPress' );
} );
@@ -133,7 +168,7 @@ describe( 'Button', () => {
it( 'should show the tooltip for empty children', () => {
const iconButton = shallow(
- );
+ ).find( 'Tooltip' );
expect( iconButton.name() ).toBe( 'Tooltip' );
expect( iconButton.prop( 'text' ) ).toBe( 'WordPress' );
} );
@@ -143,7 +178,7 @@ describe( 'Button', () => {
Children
- );
+ ).find( 'button' );
expect( iconButton.name() ).toBe( 'button' );
} );
@@ -152,14 +187,16 @@ describe( 'Button', () => {
Children
- );
+ ).find( 'Tooltip' );
expect( iconButton.name() ).toBe( 'Tooltip' );
} );
} );
describe( 'with href property', () => {
it( 'should render a link instead of a button with href prop', () => {
- const button = shallow( );
+ const button = shallow(
+
+ ).find( 'a' );
expect( button.type() ).toBe( 'a' );
expect( button.prop( 'href' ) ).toBe( 'https://wordpress.org/' );
@@ -168,7 +205,7 @@ describe( 'Button', () => {
it( 'should allow for the passing of the target prop when a link is created', () => {
const button = shallow(
- );
+ ).find( 'a' );
expect( button.prop( 'target' ) ).toBe( '_blank' );
} );
@@ -176,7 +213,7 @@ describe( 'Button', () => {
it( 'should become a button again when disabled is supplied', () => {
const button = shallow(
- );
+ ).find( 'button' );
expect( button.type() ).toBe( 'button' );
} );
diff --git a/packages/components/src/color-palette/test/__snapshots__/index.js.snap b/packages/components/src/color-palette/test/__snapshots__/index.js.snap
index 93e90b9b95908..1db3cdb27938f 100644
--- a/packages/components/src/color-palette/test/__snapshots__/index.js.snap
+++ b/packages/components/src/color-palette/test/__snapshots__/index.js.snap
@@ -87,16 +87,19 @@ exports[`ColorPalette Dropdown .renderContent should render dropdown content 1`]
`;
exports[`ColorPalette Dropdown .renderToggle should render dropdown content 1`] = `
-
- Custom color
-
+
+
+ Custom color
+
+
`;
exports[`ColorPalette Dropdown should render it correctly 1`] = `
@@ -117,6 +120,7 @@ exports[`ColorPalette Dropdown should render it correctly 1`] = `
onClick={[Function]}
>
{
} );
test( 'should call onToggle on click.', () => {
- renderedToggleButton.simulate( 'click' );
+ renderedToggleButton.find( 'button' ).simulate( 'click' );
expect( onToggle ).toHaveBeenCalledTimes( 1 );
} );
diff --git a/packages/components/src/color-picker/test/__snapshots__/index.js.snap b/packages/components/src/color-picker/test/__snapshots__/index.js.snap
index 4fdc373fe966b..e523f743a0ddd 100644
--- a/packages/components/src/color-picker/test/__snapshots__/index.js.snap
+++ b/packages/components/src/color-picker/test/__snapshots__/index.js.snap
@@ -162,6 +162,7 @@ exports[`ColorPicker should commit changes to all views on blur 1`] = `
className="components-color-picker__inputs-toggle-wrapper"
>
- { children }
-
- );
- }
+ return (
+
+ { children }
+
+ );
}
function ControlPointButton( {
@@ -83,23 +70,17 @@ function ControlPointButton( {
position,
color,
onChange,
- gradientIndex,
- gradientAST,
...additionalProps
} ) {
const instanceId = useInstanceId( ControlPointButton );
const descriptionId = `components-custom-gradient-picker__control-point-button-description-${ instanceId }`;
return (
-
+
@@ -127,12 +108,11 @@ function ControlPointButton( {
);
}
-export default function ControlPoints( {
+function ControlPoints( {
gradientPickerDomRef,
ignoreMarkerPosition,
- markerPoints,
+ value: controlPoints,
onChange,
- gradientAST,
onStartControlPointChange,
onStopControlPointChange,
} ) {
@@ -145,36 +125,21 @@ export default function ControlPoints( {
GRADIENT_MARKERS_WIDTH
);
const {
- gradientAST: referenceGradientAST,
- position,
+ initialPosition,
+ index,
significantMoveHappened,
} = controlPointMoveState.current;
- if ( ! significantMoveHappened ) {
- const initialPosition =
- referenceGradientAST.colorStops[ position ].length.value;
- if (
- Math.abs( initialPosition - relativePosition ) >=
- MINIMUM_SIGNIFICANT_MOVE
- ) {
- controlPointMoveState.current.significantMoveHappened = true;
- }
- }
-
if (
- ! isControlPointOverlapping(
- referenceGradientAST,
- relativePosition,
- position
- )
+ ! significantMoveHappened &&
+ Math.abs( initialPosition - relativePosition ) >=
+ MINIMUM_SIGNIFICANT_MOVE
) {
- onChange(
- getGradientWithPositionAtIndexChanged(
- referenceGradientAST,
- position,
- relativePosition
- )
- );
+ controlPointMoveState.current.significantMoveHappened = true;
}
+
+ onChange(
+ updateControlPointPosition( controlPoints, index, relativePosition )
+ );
};
const cleanEventListeners = () => {
@@ -197,10 +162,10 @@ export default function ControlPoints( {
};
}, [] );
- return markerPoints.map(
- ( point, index ) =>
- point &&
- ignoreMarkerPosition !== point.positionValue && (
+ return controlPoints.map( ( point, index ) => {
+ const initialPosition = point?.position;
+ return (
+ ignoreMarkerPosition !== initialPosition && (
{
if ( window && window.addEventListener ) {
controlPointMoveState.current = {
- gradientAST,
- position: index,
+ initialPosition,
+ index,
significantMoveHappened: false,
listenersActivated: true,
};
@@ -244,21 +209,27 @@ export default function ControlPoints( {
isOpen={ isOpen }
position={ point.position }
color={ point.color }
- onChange={ onChange }
- gradientAST={ gradientAST }
- gradientIndex={ index }
+ onChange={ ( newPosition ) => {
+ onChange(
+ updateControlPointPosition(
+ controlPoints,
+ index,
+ newPosition
+ )
+ );
+ } }
/>
) }
renderContent={ ( { onClose } ) => (
<>
{
+ onChangeComplete={ ( { color } ) => {
onChange(
- getGradientWithColorAtIndexChanged(
- gradientAST,
+ updateControlPointColor(
+ controlPoints,
index,
- rgb
+ color.toRgbString()
)
);
} }
@@ -267,8 +238,8 @@ export default function ControlPoints( {
className="components-custom-gradient-picker__remove-control-point"
onClick={ () => {
onChange(
- getGradientWithControlPointRemoved(
- gradientAST,
+ removeControlPoint(
+ controlPoints,
index
)
);
@@ -283,5 +254,75 @@ export default function ControlPoints( {
popoverProps={ COLOR_POPOVER_PROPS }
/>
)
+ );
+ } );
+}
+
+function InsertPoint( {
+ value: controlPoints,
+ onChange,
+ onOpenInserter,
+ onCloseInserter,
+ insertPosition,
+} ) {
+ const [ alreadyInsertedPoint, setAlreadyInsertedPoint ] = useState( false );
+ return (
+ {
+ onCloseInserter();
+ } }
+ renderToggle={ ( { isOpen, onToggle } ) => (
+ {
+ if ( isOpen ) {
+ onCloseInserter();
+ } else {
+ setAlreadyInsertedPoint( false );
+ onOpenInserter();
+ }
+ onToggle();
+ } }
+ className="components-custom-gradient-picker__insert-point"
+ icon={ plus }
+ style={ {
+ left:
+ insertPosition !== null
+ ? `${ insertPosition }%`
+ : undefined,
+ } }
+ />
+ ) }
+ renderContent={ () => (
+ {
+ if ( ! alreadyInsertedPoint ) {
+ onChange(
+ addControlPoint(
+ controlPoints,
+ insertPosition,
+ color.toRgbString()
+ )
+ );
+ setAlreadyInsertedPoint( true );
+ } else {
+ onChange(
+ updateControlPointColorByPosition(
+ controlPoints,
+ insertPosition,
+ color.toRgbString()
+ )
+ );
+ }
+ } }
+ />
+ ) }
+ popoverProps={ COLOR_POPOVER_PROPS }
+ />
);
}
+ControlPoints.InsertPoint = InsertPoint;
+
+export default ControlPoints;
diff --git a/packages/components/src/custom-gradient-picker/custom-gradient-bar.js b/packages/components/src/custom-gradient-bar/index.js
similarity index 55%
rename from packages/components/src/custom-gradient-picker/custom-gradient-bar.js
rename to packages/components/src/custom-gradient-bar/index.js
index f73a4bf5c3e59..bd8b1922cdf6e 100644
--- a/packages/components/src/custom-gradient-picker/custom-gradient-bar.js
+++ b/packages/components/src/custom-gradient-bar/index.js
@@ -8,94 +8,17 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { useRef, useReducer, useState } from '@wordpress/element';
-import { plus } from '@wordpress/icons';
+import { useRef, useReducer } from '@wordpress/element';
/**
* Internal dependencies
*/
-import Button from '../button';
-import ColorPicker from '../color-picker';
-import Dropdown from '../dropdown';
import ControlPoints from './control-points';
+import { getHorizontalRelativeGradientPosition } from './utils';
import {
INSERT_POINT_WIDTH,
- COLOR_POPOVER_PROPS,
MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_POINT,
} from './constants';
-import { serializeGradient } from './serializer';
-import {
- getGradientWithColorAtPositionChanged,
- getGradientWithColorStopAdded,
- getHorizontalRelativeGradientPosition,
- getMarkerPoints,
- getGradientParsed,
- getLinearGradientRepresentationOfARadial,
-} from './utils';
-
-function InsertPoint( {
- onChange,
- gradientAST,
- onOpenInserter,
- onCloseInserter,
- insertPosition,
-} ) {
- const [ alreadyInsertedPoint, setAlreadyInsertedPoint ] = useState( false );
- return (
- {
- onCloseInserter();
- } }
- renderToggle={ ( { isOpen, onToggle } ) => (
- {
- if ( isOpen ) {
- onCloseInserter();
- } else {
- setAlreadyInsertedPoint( false );
- onOpenInserter();
- }
- onToggle();
- } }
- className="components-custom-gradient-picker__insert-point"
- icon={ plus }
- style={ {
- left:
- insertPosition !== null
- ? `${ insertPosition }%`
- : undefined,
- } }
- />
- ) }
- renderContent={ () => (
- {
- let newGradient;
- if ( alreadyInsertedPoint ) {
- newGradient = getGradientWithColorAtPositionChanged(
- gradientAST,
- insertPosition,
- rgb
- );
- } else {
- newGradient = getGradientWithColorStopAdded(
- gradientAST,
- insertPosition,
- rgb
- );
- setAlreadyInsertedPoint( true );
- }
- onChange( newGradient );
- } }
- />
- ) }
- popoverProps={ COLOR_POPOVER_PROPS }
- />
- );
-}
function customGradientBarReducer( state, action ) {
switch ( action.type ) {
@@ -148,17 +71,13 @@ function customGradientBarReducer( state, action ) {
}
const customGradientBarReducerInitialState = { id: 'IDLE' };
-export default function CustomGradientBar( { value, onChange } ) {
- const { gradientAST, gradientValue, hasGradient } = getGradientParsed(
- value
- );
-
- const onGradientStructureChange = ( newGradientStructure ) => {
- onChange( serializeGradient( newGradientStructure ) );
- };
-
+export default function CustomGradientBar( {
+ background,
+ hasGradient,
+ value: controlPoints,
+ onChange,
+} ) {
const gradientPickerDomRef = useRef();
- const markerPoints = getMarkerPoints( gradientAST );
const [ gradientBarState, gradientBarStateDispatch ] = useReducer(
customGradientBarReducer,
@@ -173,9 +92,9 @@ export default function CustomGradientBar( { value, onChange } ) {
// If the insert point is close to an existing control point don't show it.
if (
- some( markerPoints, ( { positionValue } ) => {
+ some( controlPoints, ( { position } ) => {
return (
- Math.abs( insertPosition - positionValue ) <
+ Math.abs( insertPosition - position ) <
MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_POINT
);
} )
@@ -206,24 +125,15 @@ export default function CustomGradientBar( { value, onChange } ) {
) }
onMouseEnter={ onMouseEnterAndMove }
onMouseMove={ onMouseEnterAndMove }
- // On radial gradients the bar should display a linear gradient.
- // On radial gradients the bar represents a slice of the gradient from the center until the outside.
- style={ {
- background:
- gradientAST.type === 'radial-gradient'
- ? getLinearGradientRepresentationOfARadial(
- gradientAST
- )
- : gradientValue,
- } }
+ style={ { background } }
onMouseLeave={ onMouseLeave }
>
{ ( isMovingInserter || isInsertingControlPoint ) && (
-
{
gradientBarStateDispatch( {
type: 'OPEN_INSERTER',
@@ -243,9 +153,8 @@ export default function CustomGradientBar( { value, onChange } ) {
? gradientBarState.insertPosition
: undefined
}
- markerPoints={ markerPoints }
- onChange={ onGradientStructureChange }
- gradientAST={ gradientAST }
+ value={ controlPoints }
+ onChange={ onChange }
onStartControlPointChange={ () => {
gradientBarStateDispatch( {
type: 'START_CONTROL_CHANGE',
diff --git a/packages/components/src/custom-gradient-bar/utils.js b/packages/components/src/custom-gradient-bar/utils.js
new file mode 100644
index 0000000000000..eb088b08ea0a5
--- /dev/null
+++ b/packages/components/src/custom-gradient-bar/utils.js
@@ -0,0 +1,199 @@
+/**
+ * Internal dependencies
+ */
+import {
+ MINIMUM_DISTANCE_BETWEEN_POINTS,
+ MINIMUM_ABSOLUTE_LEFT_POSITION,
+ INSERT_POINT_WIDTH,
+} from './constants';
+
+/**
+ * Control point for the gradient bar.
+ *
+ * @typedef {Object} ControlPoint
+ * @property {string} color Color of the control point.
+ * @property {number} position Integer position of the control point as a percentage.
+ */
+
+/**
+ * Color as parsed from the gradient by gradient-parser.
+ *
+ * @typedef {Object} Color
+ * @property {string} r Red component.
+ * @property {string} g Green component.
+ * @property {string} b Green component.
+ * @property {string} [a] Optional alpha component.
+ */
+
+/**
+ * Clamps a number between 0 and 100.
+ *
+ * @param {number} value Value to clamp.
+ *
+ * @return {number} Value clamped between 0 and 100.
+ */
+export function clampPercent( value ) {
+ return Math.max( 0, Math.min( 100, value ) );
+}
+
+/**
+ * Check if a control point is overlapping with another.
+ *
+ * @param {ControlPoint[]} value Array of control points.
+ * @param {number} initialIndex Index of the position to test.
+ * @param {number} newPosition New position of the control point.
+ * @param {number} minDistance Distance considered to be overlapping.
+ *
+ * @return {boolean} True if the point is overlapping.
+ */
+export function isOverlapping(
+ value,
+ initialIndex,
+ newPosition,
+ minDistance = MINIMUM_DISTANCE_BETWEEN_POINTS
+) {
+ const initialPosition = value[ initialIndex ].position;
+ const minPosition = Math.min( initialPosition, newPosition );
+ const maxPosition = Math.max( initialPosition, newPosition );
+
+ return value.some( ( { position }, index ) => {
+ return (
+ index !== initialIndex &&
+ ( Math.abs( position - newPosition ) < minDistance ||
+ ( minPosition < position && position < maxPosition ) )
+ );
+ } );
+}
+
+/**
+ * Adds a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} position Position to insert the new point.
+ * @param {Color} color Color to update the control point at index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function addControlPoint( points, position, color ) {
+ const nextIndex = points.findIndex(
+ ( point ) => point.position > position
+ );
+ const newPoint = { color, position };
+ const newPoints = points.slice();
+ newPoints.splice( nextIndex - 1, 0, newPoint );
+ return newPoints;
+}
+
+/**
+ * Removes a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} index Index to remove.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function removeControlPoint( points, index ) {
+ return points.filter( ( point, pointIndex ) => {
+ return pointIndex !== index;
+ } );
+}
+
+/**
+ * Updates a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} index Index to update.
+ * @param {ControlPoint[]} newPoint New control point to replace the index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function updateControlPoint( points, index, newPoint ) {
+ const newValue = points.slice();
+ newValue[ index ] = newPoint;
+ return newValue;
+}
+
+/**
+ * Updates the position of a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} index Index to update.
+ * @param {number} newPosition Position to move the control point at index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function updateControlPointPosition( points, index, newPosition ) {
+ if ( isOverlapping( points, index, newPosition ) ) {
+ return points;
+ }
+ const newPoint = {
+ ...points[ index ],
+ position: newPosition,
+ };
+ return updateControlPoint( points, index, newPoint );
+}
+
+/**
+ * Updates the position of a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} index Index to update.
+ * @param {Color} newColor Color to update the control point at index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function updateControlPointColor( points, index, newColor ) {
+ const newPoint = {
+ ...points[ index ],
+ color: newColor,
+ };
+ return updateControlPoint( points, index, newPoint );
+}
+
+/**
+ * Updates the position of a control point from an array and returns the new array.
+ *
+ * @param {ControlPoint[]} points Array of control points.
+ * @param {number} position Position of the color stop.
+ * @param {string} newColor Color to update the control point at index.
+ *
+ * @return {ControlPoint[]} New array of control points.
+ */
+export function updateControlPointColorByPosition(
+ points,
+ position,
+ newColor
+) {
+ const index = points.findIndex( ( point ) => point.position === position );
+ return updateControlPointColor( points, index, newColor );
+}
+
+/**
+ * Gets the horizontal coordinate when dragging a control point with the mouse.
+ *
+ * @param {number} mouseXCoordinate Horizontal coordinate of the mouse position.
+ * @param {Element} containerElement Container for the gradient picker.
+ * @param {number} positionedElementWidth Width of the positioned element.
+ *
+ * @return {number} Whole number percentage from the left.
+ */
+export function getHorizontalRelativeGradientPosition(
+ mouseXCoordinate,
+ containerElement,
+ positionedElementWidth
+) {
+ if ( ! containerElement ) {
+ return;
+ }
+ const { x, width } = containerElement.getBoundingClientRect();
+ const absolutePositionValue =
+ mouseXCoordinate -
+ x -
+ MINIMUM_ABSOLUTE_LEFT_POSITION -
+ positionedElementWidth / 2;
+ const availableWidth =
+ width - MINIMUM_ABSOLUTE_LEFT_POSITION - INSERT_POINT_WIDTH;
+ return Math.round(
+ clampPercent( ( absolutePositionValue * 100 ) / availableWidth )
+ );
+}
diff --git a/packages/components/src/custom-gradient-picker/constants.js b/packages/components/src/custom-gradient-picker/constants.js
index 8646c93471b19..9924ff2b3d686 100644
--- a/packages/components/src/custom-gradient-picker/constants.js
+++ b/packages/components/src/custom-gradient-picker/constants.js
@@ -3,22 +3,11 @@
*/
import { __ } from '@wordpress/i18n';
-export const INSERT_POINT_WIDTH = 23;
-export const GRADIENT_MARKERS_WIDTH = 18;
-export const MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_MARKER =
- ( INSERT_POINT_WIDTH + GRADIENT_MARKERS_WIDTH ) / 2;
-export const MINIMUM_ABSOLUTE_LEFT_POSITION = 5;
-export const MINIMUM_DISTANCE_BETWEEN_POINTS = 0;
-export const MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_POINT = 10;
-export const KEYBOARD_CONTROL_POINT_VARIATION = MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_POINT;
-export const MINIMUM_SIGNIFICANT_MOVE = 5;
export const DEFAULT_GRADIENT =
'linear-gradient(135deg, rgba(6, 147, 227, 1) 0%, rgb(155, 81, 224) 100%)';
-export const COLOR_POPOVER_PROPS = {
- className: 'components-custom-gradient-picker__color-picker-popover',
- position: 'top',
-};
+
export const DEFAULT_LINEAR_GRADIENT_ANGLE = 180;
+
export const HORIZONTAL_GRADIENT_ORIENTATION = {
type: 'angular',
value: 90,
@@ -28,3 +17,18 @@ export const GRADIENT_OPTIONS = [
{ value: 'linear-gradient', label: __( 'Linear' ) },
{ value: 'radial-gradient', label: __( 'Radial' ) },
];
+
+export const DIRECTIONAL_ORIENTATION_ANGLE_MAP = {
+ top: 0,
+ 'top right': 45,
+ 'right top': 45,
+ right: 90,
+ 'right bottom': 135,
+ 'bottom right': 135,
+ bottom: 180,
+ 'bottom left': 225,
+ 'left bottom': 225,
+ left: 270,
+ 'top left': 315,
+ 'left top': 315,
+};
diff --git a/packages/components/src/custom-gradient-picker/index.js b/packages/components/src/custom-gradient-picker/index.js
index 39e63ab404ed2..7d10440295afe 100644
--- a/packages/components/src/custom-gradient-picker/index.js
+++ b/packages/components/src/custom-gradient-picker/index.js
@@ -12,15 +12,21 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import AnglePickerControl from '../angle-picker-control';
-import CustomGradientBar from './custom-gradient-bar';
+import CustomGradientBar from '../custom-gradient-bar';
import { Flex } from '../flex';
import SelectControl from '../select-control';
-import { getGradientParsed } from './utils';
+import {
+ getGradientAstWithDefault,
+ getLinearGradientRepresentationOfARadial,
+ getGradientAstWithControlPoints,
+ getStopCssColor,
+} from './utils';
import { serializeGradient } from './serializer';
import {
DEFAULT_LINEAR_GRADIENT_ANGLE,
HORIZONTAL_GRADIENT_ORIENTATION,
GRADIENT_OPTIONS,
+ DEFAULT_GRADIENT,
} from './constants';
import {
AccessoryWrapper,
@@ -98,11 +104,38 @@ const GradientTypePicker = ( { gradientAST, hasGradient, onChange } ) => {
};
export default function CustomGradientPicker( { value, onChange } ) {
- const { gradientAST, hasGradient } = getGradientParsed( value );
- const { type } = gradientAST;
+ const gradientAST = getGradientAstWithDefault( value );
+ // On radial gradients the bar should display a linear gradient.
+ // On radial gradients the bar represents a slice of the gradient from the center until the outside.
+ const background =
+ gradientAST.type === 'radial-gradient'
+ ? getLinearGradientRepresentationOfARadial( gradientAST )
+ : gradientAST.value;
+ const hasGradient = gradientAST.value !== DEFAULT_GRADIENT;
+ // Control points color option may be hex from presets, custom colors will be rgb.
+ // The position should always be a percentage.
+ const controlPoints = gradientAST.colorStops.map( ( colorStop ) => ( {
+ color: getStopCssColor( colorStop ),
+ position: parseInt( colorStop.length.value ),
+ } ) );
+
return (
-
+
{
+ onChange(
+ serializeGradient(
+ getGradientAstWithControlPoints(
+ gradientAST,
+ newControlPoints
+ )
+ )
+ );
+ } }
+ />
- { type === 'linear-gradient' && (
+ { gradientAST.type === 'linear-gradient' && (
{
- if ( colorStopIndex !== index ) {
- return colorStop;
- }
- return {
- ...colorStop,
- length: {
- ...colorStop.length,
- value: relativePosition,
- },
- };
- }
- ),
- };
-}
-
-export function isControlPointOverlapping(
- gradientAST,
- position,
- initialIndex
-) {
- const initialPosition = parseInt(
- gradientAST.colorStops[ initialIndex ].length.value
- );
- const minPosition = Math.min( initialPosition, position );
- const maxPosition = Math.max( initialPosition, position );
-
- return some( gradientAST.colorStops, ( { length }, index ) => {
- const itemPosition = parseInt( length.value );
- return (
- index !== initialIndex &&
- ( Math.abs( itemPosition - position ) <
- MINIMUM_DISTANCE_BETWEEN_POINTS ||
- ( minPosition < itemPosition && itemPosition < maxPosition ) )
- );
- } );
-}
-
-function getGradientWithPositionAtIndexSummed(
- gradientAST,
- index,
- valueToSum
-) {
- const currentPosition = gradientAST.colorStops[ index ].length.value;
- const newPosition = Math.max(
- 0,
- Math.min( 100, parseInt( currentPosition ) + valueToSum )
- );
- if ( isControlPointOverlapping( gradientAST, newPosition, index ) ) {
- return gradientAST;
- }
- return getGradientWithPositionAtIndexChanged(
- gradientAST,
- index,
- newPosition
- );
-}
-
-export function getGradientWithPositionAtIndexIncreased( gradientAST, index ) {
- return getGradientWithPositionAtIndexSummed(
- gradientAST,
- index,
- KEYBOARD_CONTROL_POINT_VARIATION
- );
-}
-
-export function getGradientWithPositionAtIndexDecreased( gradientAST, index ) {
- return getGradientWithPositionAtIndexSummed(
- gradientAST,
- index,
- -KEYBOARD_CONTROL_POINT_VARIATION
- );
-}
-
-export function getGradientWithColorAtIndexChanged(
- gradientAST,
- index,
- rgbaColor
-) {
- return {
- ...gradientAST,
- colorStops: gradientAST.colorStops.map(
- ( colorStop, colorStopIndex ) => {
- if ( colorStopIndex !== index ) {
- return colorStop;
- }
- return {
- ...colorStop,
- ...tinyColorRgbToGradientColorStop( rgbaColor ),
- };
- }
- ),
- };
-}
-
-export function getGradientWithColorAtPositionChanged(
- gradientAST,
- relativePositionValue,
- rgbaColor
-) {
- const index = findIndex( gradientAST.colorStops, ( colorStop ) => {
- return (
- colorStop &&
- colorStop.length &&
- colorStop.length.type === '%' &&
- colorStop.length.value === relativePositionValue.toString()
- );
- } );
- return getGradientWithColorAtIndexChanged( gradientAST, index, rgbaColor );
-}
-
-export function getGradientWithControlPointRemoved( gradientAST, index ) {
- return {
- ...gradientAST,
- colorStops: gradientAST.colorStops.filter( ( elem, elemIndex ) => {
- return elemIndex !== index;
- } ),
- };
-}
-
-export function getHorizontalRelativeGradientPosition(
- mouseXCoordinate,
- containerElement,
- positionedElementWidth
-) {
- if ( ! containerElement ) {
- return;
- }
- const { x, width } = containerElement.getBoundingClientRect();
- const absolutePositionValue =
- mouseXCoordinate -
- x -
- MINIMUM_ABSOLUTE_LEFT_POSITION -
- positionedElementWidth / 2;
- const availableWidth =
- width - MINIMUM_ABSOLUTE_LEFT_POSITION - INSERT_POINT_WIDTH;
- return Math.round(
- Math.min(
- Math.max( ( absolutePositionValue * 100 ) / availableWidth, 0 ),
- 100
- )
- );
-}
-
-/**
- * Returns the marker points from a gradient AST.
- *
- * @param {Object} gradientAST An object representing the gradient AST.
- *
- * @return {Array.<{color: string, position: string, positionValue: number}>}
- * An array of markerPoint objects.
- * color: A string with the color code ready to be used in css style e.g: "rgba( 1, 2 , 3, 0.5)".
- * position: A string with the position ready to be used in css style e.g: "70%".
- * positionValue: A number with the relative position value e.g: 70.
- */
-export function getMarkerPoints( gradientAST ) {
- if ( ! gradientAST ) {
- return [];
- }
- return map( gradientAST.colorStops, ( colorStop ) => {
- if (
- ! colorStop ||
- ! colorStop.length ||
- colorStop.length.type !== '%'
- ) {
- return null;
- }
- return {
- color: serializeGradientColor( colorStop ),
- position: serializeGradientPosition( colorStop.length ),
- positionValue: parseInt( colorStop.length.value ),
- };
- } );
-}
+import { serializeGradient } from './serializer';
export function getLinearGradientRepresentationOfARadial( gradientAST ) {
return serializeGradient( {
@@ -241,55 +22,24 @@ export function getLinearGradientRepresentationOfARadial( gradientAST ) {
} );
}
-const DIRECTIONAL_ORIENTATION_ANGLE_MAP = {
- top: 0,
- 'top right': 45,
- 'right top': 45,
- right: 90,
- 'right bottom': 135,
- 'bottom right': 135,
- bottom: 180,
- 'bottom left': 225,
- 'left bottom': 225,
- left: 270,
- 'top left': 315,
- 'left top': 315,
-};
-
function hasUnsupportedLength( item ) {
return item.length === undefined || item.length.type !== '%';
}
-function assignColorStopLengths( gradientAST ) {
- const { colorStops } = gradientAST;
- const step = 100 / ( colorStops.length - 1 );
- colorStops.forEach( ( stop, index ) => {
- stop.length = {
- value: step * index,
- type: '%',
- };
- } );
-}
-
-export function getGradientParsed( value ) {
- let hasGradient = !! value;
+export function getGradientAstWithDefault( value ) {
// gradientAST will contain the gradient AST as parsed by gradient-parser npm module.
// More information of its structure available at https://www.npmjs.com/package/gradient-parser#ast.
let gradientAST;
- let gradientValue;
+
try {
- gradientAST = gradientParser.parse( value || DEFAULT_GRADIENT )[ 0 ];
- gradientValue = value || DEFAULT_GRADIENT;
+ gradientAST = gradientParser.parse( value )[ 0 ];
+ gradientAST.value = value;
} catch ( error ) {
- hasGradient = false;
gradientAST = gradientParser.parse( DEFAULT_GRADIENT )[ 0 ];
- gradientValue = DEFAULT_GRADIENT;
+ gradientAST.value = DEFAULT_GRADIENT;
}
- if (
- gradientAST.orientation &&
- gradientAST.orientation.type === 'directional'
- ) {
+ if ( gradientAST.orientation?.type === 'directional' ) {
gradientAST.orientation.type = 'angular';
gradientAST.orientation.value = DIRECTIONAL_ORIENTATION_ANGLE_MAP[
gradientAST.orientation.value
@@ -297,13 +47,52 @@ export function getGradientParsed( value ) {
}
if ( gradientAST.colorStops.some( hasUnsupportedLength ) ) {
- assignColorStopLengths( gradientAST );
- gradientValue = serializeGradient( gradientAST );
+ const { colorStops } = gradientAST;
+ const step = 100 / ( colorStops.length - 1 );
+ colorStops.forEach( ( stop, index ) => {
+ stop.length = {
+ value: step * index,
+ type: '%',
+ };
+ } );
+ gradientAST.value = serializeGradient( gradientAST );
}
+ return gradientAST;
+}
+
+export function getGradientAstWithControlPoints(
+ gradientAST,
+ newControlPoints
+) {
return {
- hasGradient,
- gradientAST,
- gradientValue,
+ ...gradientAST,
+ colorStops: newControlPoints.map( ( { position, color } ) => {
+ const { r, g, b, a } = tinycolor( color ).toRgb();
+ return {
+ length: {
+ type: '%',
+ value: position.toString(),
+ },
+ type: a < 1 ? 'rgba' : 'rgb',
+ value: a < 1 ? [ r, g, b, a ] : [ r, g, b ],
+ };
+ } ),
};
}
+
+export function getStopCssColor( colorStop ) {
+ switch ( colorStop.type ) {
+ case 'hex':
+ return `#${ colorStop.value }`;
+ case 'literal':
+ return colorStop.value;
+ case 'rgb':
+ case 'rgba':
+ return `${ colorStop.type }(${ colorStop.value.join( ',' ) })`;
+ default:
+ // Should be unreachable if passing an AST from gradient-parser.
+ // See https://github.com/rafaelcaricio/gradient-parser#ast.
+ return 'transparent';
+ }
+}
diff --git a/packages/components/src/disclosure/index.js b/packages/components/src/disclosure/index.js
new file mode 100644
index 0000000000000..5458ba053eef6
--- /dev/null
+++ b/packages/components/src/disclosure/index.js
@@ -0,0 +1,11 @@
+/**
+ * Accessible Disclosure component that controls visibility of a section of
+ * content. It follows the WAI-ARIA Disclosure Pattern.
+ *
+ * @see https://reakit.io/docs/disclosure/
+ *
+ * The plan is to build own API that accounts for future breaking changes
+ * in Reakit (https://github.com/WordPress/gutenberg/pull/28085).
+ */
+/* eslint-disable-next-line no-restricted-imports */
+export { DisclosureContent } from 'reakit';
diff --git a/packages/components/src/dropdown-menu/index.js b/packages/components/src/dropdown-menu/index.js
index c6e4c69436e18..74796e1c29dea 100644
--- a/packages/components/src/dropdown-menu/index.js
+++ b/packages/components/src/dropdown-menu/index.js
@@ -9,6 +9,7 @@ import { flatMap, isEmpty, isFunction } from 'lodash';
*/
import { DOWN } from '@wordpress/keycodes';
import deprecated from '@wordpress/deprecated';
+import { menu } from '@wordpress/icons';
/**
* Internal dependencies
@@ -37,7 +38,7 @@ function DropdownMenu( {
children,
className,
controls,
- icon = 'menu',
+ icon = menu,
label,
popoverProps,
toggleProps,
diff --git a/packages/components/src/form-toggle/README.md b/packages/components/src/form-toggle/README.md
index 7db5d8c412bd4..4a00a62f2e12f 100644
--- a/packages/components/src/form-toggle/README.md
+++ b/packages/components/src/form-toggle/README.md
@@ -79,6 +79,13 @@ If no value is passed the toggle will be unchecked.
- Type: `Boolean`
- Required: No
+#### disabled
+
+If disabled is true the toggle will be disabled and apply the appropriate styles.
+
+- Type: `Boolean`
+- Required: No
+
#### onChange
A function that receives the checked state (boolean) as input.
diff --git a/packages/components/src/form-toggle/index.js b/packages/components/src/form-toggle/index.js
index 053addfe32540..4e5de8f4fdb9e 100644
--- a/packages/components/src/form-toggle/index.js
+++ b/packages/components/src/form-toggle/index.js
@@ -4,9 +4,17 @@
import classnames from 'classnames';
import { noop } from 'lodash';
-function FormToggle( { className, checked, id, onChange = noop, ...props } ) {
+function FormToggle( {
+ className,
+ checked,
+ id,
+ disabled,
+ onChange = noop,
+ ...props
+} ) {
const wrapperClasses = classnames( 'components-form-toggle', className, {
'is-checked': checked,
+ 'is-disabled': disabled,
} );
return (
@@ -17,6 +25,7 @@ function FormToggle( { className, checked, id, onChange = noop, ...props } ) {
type="checkbox"
checked={ checked }
onChange={ onChange }
+ disabled={ disabled }
{ ...props }
/>
diff --git a/packages/components/src/form-toggle/style.scss b/packages/components/src/form-toggle/style.scss
index 3805880b84e82..d4b74aaa1f4f5 100644
--- a/packages/components/src/form-toggle/style.scss
+++ b/packages/components/src/form-toggle/style.scss
@@ -60,6 +60,7 @@ $toggle-border-width: 1px;
}
// Disabled state:
+ &.is-disabled,
.components-disabled & {
opacity: 0.3;
}
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index aaf2620e25923..9911aa6faa887 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -35,11 +35,18 @@ export { default as ColorIndicator } from './color-indicator';
export { default as ColorPalette } from './color-palette';
export { default as ColorPicker } from './color-picker';
export { default as ComboboxControl } from './combobox-control';
+export {
+ Composite as __unstableComposite,
+ CompositeGroup as __unstableCompositeGroup,
+ CompositeItem as __unstableCompositeItem,
+ useCompositeState as __unstableUseCompositeState,
+} from './composite';
export { default as CustomSelectControl } from './custom-select-control';
export { default as Dashicon } from './dashicon';
export { default as DateTimePicker, DatePicker, TimePicker } from './date-time';
export { default as __experimentalDimensionControl } from './dimension-control';
export { default as Disabled } from './disabled';
+export { DisclosureContent as __unstableDisclosureContent } from './disclosure';
export { default as Draggable } from './draggable';
export {
default as DropZone,
diff --git a/packages/components/src/navigation/styles/navigation-styles.js b/packages/components/src/navigation/styles/navigation-styles.js
index 4e5c76ad36206..0b4fbcb1a4201 100644
--- a/packages/components/src/navigation/styles/navigation-styles.js
+++ b/packages/components/src/navigation/styles/navigation-styles.js
@@ -159,6 +159,8 @@ export const ItemBaseUI = styled.li`
button,
a {
+ width: 100%;
+
&:hover,
&:focus:not( [aria-disabled='true'] ):active,
&:active:not( [aria-disabled='true'] ):active {
diff --git a/packages/components/src/toggle-control/README.md b/packages/components/src/toggle-control/README.md
index 1413733c4628d..8aabf698ca0ef 100644
--- a/packages/components/src/toggle-control/README.md
+++ b/packages/components/src/toggle-control/README.md
@@ -48,6 +48,14 @@ If no value is passed the toggle will be unchecked.
- Type: `Boolean`
- Required: No
+### disabled
+
+If disabled is true the toggle will be disabled and apply the appropriate styles.
+
+- Type: `Boolean`
+- Required: No
+
+
### onChange
A function that receives the checked state (boolean) as input.
diff --git a/packages/components/src/toggle-control/index.js b/packages/components/src/toggle-control/index.js
index 159c920b15641..16615c4fb3282 100644
--- a/packages/components/src/toggle-control/index.js
+++ b/packages/components/src/toggle-control/index.js
@@ -21,6 +21,7 @@ export default function ToggleControl( {
help,
className,
onChange,
+ disabled,
} ) {
function onChangeToggle( event ) {
onChange( event.target.checked );
@@ -45,6 +46,7 @@ export default function ToggleControl( {
checked={ checked }
onChange={ onChangeToggle }
aria-describedby={ describedBy }
+ disabled={ disabled }
/>