diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index 5163a343541ca7..43e65749baae9c 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -183,7 +183,7 @@ jobs: NO_CHECKS: 'true' - name: Upload artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: name: gutenberg-plugin path: ./gutenberg.zip @@ -206,7 +206,7 @@ jobs: - name: Upload release notes artifact if: ${{ needs.bump-version.outputs.new_version }} - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: name: release-notes path: ./release-notes.txt @@ -270,12 +270,12 @@ jobs: run: echo "version=$(echo $VERSION | cut -d / -f 3 | sed 's/-rc./ RC/' )" >> $GITHUB_OUTPUT - name: Download Plugin Zip Artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: gutenberg-plugin - name: Download Release Notes Artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: release-notes diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 362aec73cd6241..f4e470eb177f93 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -41,7 +41,7 @@ jobs: npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" - name: Archive debug artifacts (screenshots, HTML snapshots) - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: always() with: name: failures-artifacts @@ -49,7 +49,7 @@ jobs: if-no-files-found: ignore - name: Archive flaky tests report - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: always() with: name: flaky-tests-report @@ -92,7 +92,7 @@ jobs: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:playwright -- --shard=${{ matrix.part }}/${{ matrix.totalParts }} - name: Archive debug artifacts (screenshots, traces) - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: always() with: name: failures-artifacts @@ -100,7 +100,7 @@ jobs: if-no-files-found: ignore - name: Archive flaky tests report - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: always() with: name: flaky-tests-report @@ -120,7 +120,7 @@ jobs: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - - uses: actions/download-artifact@v4.1.0 + - uses: actions/download-artifact@v4.1.1 id: download_artifact # Don't fail the job if there isn't any flaky tests report. continue-on-error: true diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index bc457758b8385b..ade7ecd3e9a060 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -9,4 +9,4 @@ jobs: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - - uses: gradle/wrapper-validation-action@v1 + - uses: gradle/wrapper-validation-action@v2 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 12063c0eb7d496..bef55a431cea97 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -86,7 +86,7 @@ jobs: - name: Archive performance results if: success() - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: name: performance-results path: ${{ env.WP_ARTIFACTS_PATH }}/*.performance-results*.json @@ -100,7 +100,7 @@ jobs: ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA b61dde2e5ec29d98801e623de968bfb286c5be3f $COMMITTED_AT - name: Archive debug artifacts (screenshots, HTML snapshots) - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: failure() with: name: failures-artifacts diff --git a/.github/workflows/php-changes-detection.yml b/.github/workflows/php-changes-detection.yml index fc0ba83b95dbc4..6a13d4d014fc69 100644 --- a/.github/workflows/php-changes-detection.yml +++ b/.github/workflows/php-changes-detection.yml @@ -17,7 +17,7 @@ jobs: - name: Get changed PHP files id: changed-files-php - uses: tj-actions/changed-files@716b1e13042866565e00e85fd4ec490e186c4a2f # v41.0.1 + uses: tj-actions/changed-files@90a06d6ba9543371ab4df8eeca0be07ca6054959 # v42.0.2 with: files: | *.{php} @@ -38,7 +38,7 @@ jobs: echo "formatted_change_list=$formatted_change_list" >> $GITHUB_OUTPUT - name: Find Comment - uses: peter-evans/find-comment@v2 + uses: peter-evans/find-comment@v3 id: find-comment with: issue-number: ${{ github.event.pull_request.number }} @@ -47,7 +47,7 @@ jobs: - name: Create comment if: steps.find-comment.outputs.comment-id == '' && steps.changed-files-php.outputs.any_changed == 'true' - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v4 with: issue-number: ${{ github.event.pull_request.number }} body: | @@ -67,7 +67,7 @@ jobs: - name: Update comment if: steps.find-comment.outputs.comment-id != '' && steps.changed-files-php.outputs.any_changed == 'true' - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v4 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} @@ -89,7 +89,7 @@ jobs: - name: Update comment if: steps.find-comment.outputs.comment-id != '' && steps.changed-files-php.outputs.any_changed != 'true' - uses: peter-evans/create-or-update-comment@v3 + uses: peter-evans/create-or-update-comment@v4 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/props-bot.yml b/.github/workflows/props-bot.yml new file mode 100644 index 00000000000000..0f21f47ef14f99 --- /dev/null +++ b/.github/workflows/props-bot.yml @@ -0,0 +1,88 @@ +name: Props Bot + +on: + # This event runs anytime a PR is (re)opened, updated, marked ready for review, or labeled. + # GitHub does not allow filtering the `labeled` event by a specific label. + # However, the logic below will short-circuit the workflow when the `props-bot` label is not the one being added. + # Note: The pull_request_target event is used instead of pull_request because this workflow needs permission to comment + # on the pull request. Because this event grants extra permissions to `GITHUB_TOKEN`, any code changes within the PR + # should be considered untrusted. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/. + pull_request_target: + types: + - opened + - synchronize + - reopened + - labeled + - ready_for_review + # This event runs anytime a comment is added or deleted. + # You cannot filter this event for PR comments only. + # However, the logic below does short-circuit the workflow for issues. + issue_comment: + type: + - created + # This event will run everytime a new PR review is initially submitted. + pull_request_review: + types: + - submitted + # This event runs anytime a PR review comment is created or deleted. + pull_request_review_comment: + types: + - created + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ contains( fromJSON( '["pull_request_target", "pull_request_review", "pull_request_review_comment"]' ), github.event_name ) && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Compiles a list of props for a pull request. + # + # Performs the following steps: + # - Collects a list of contributor props and leaves a comment. + # - Removes the props-bot label, if necessary. + props-bot: + name: Generate a list of props + runs-on: ubuntu-latest + permissions: + # The action needs permission `write` permission for PRs in order to add a comment. + pull-requests: write + contents: read + timeout-minutes: 20 + # The job will run when pull requests are open, ready for review and: + # + # - A comment is added to the pull request. + # - A review is created or commented on. + # - The pull request is opened, synchronized, marked ready for review, or reopened. + # - The `props-bot` label is added to the pull request. + if: | + ( + github.event_name == 'issue_comment' && github.event.issue.pull_request || + contains( fromJSON( '["pull_request_review", "pull_request_review_comment"]' ), github.event_name ) || + github.event_name == 'pull_request_target' && github.event.action != 'labeled' || + 'props-bot' == github.event.label.name + ) && + ( ! github.event.pull_request.draft && github.event.pull_request.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open' ) + + steps: + - name: Gather a list of contributors + uses: WordPress/props-bot-action@trunk + + - name: Remove the props-bot label + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + if: ${{ github.event.action == 'labeled' && 'props-bot' == github.event.label.name }} + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: '${{ github.event.number }}', + name: 'props-bot' + }); diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml index d0b8778a1d3c7c..85c71199d28e14 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -24,7 +24,7 @@ jobs: check-latest: true - name: Cache NPM packages - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index e0a3b9639cf389..7f7e641e2d2224 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -37,7 +37,7 @@ jobs: uses: ./.github/setup-node - name: Restore tests setup cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 with: path: | ~/.appium @@ -47,10 +47,10 @@ jobs: run: npm run native test:e2e:setup - name: Gradle cache - uses: gradle/gradle-build-action@982da8e78c05368c70dac0351bb82647a9e9a5d2 # v2.11.1 + uses: gradle/gradle-build-action@3b1b3b9a2104c2b47fbae53f3938079c00c9bb87 # v3.0.0 - name: AVD cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 id: avd-cache with: path: | @@ -60,7 +60,7 @@ jobs: - name: Create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@99a4aac18b4df9b3af66c4a1f04c1f23fa10c270 # v2.29.0 + uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 # v2.30.1 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -71,7 +71,7 @@ jobs: script: echo "Generated AVD snapshot for caching." - name: Run tests - uses: reactivecircus/android-emulator-runner@99a4aac18b4df9b3af66c4a1f04c1f23fa10c270 # v2.29.0 + uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 # v2.30.1 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -81,13 +81,13 @@ jobs: profile: Nexus 6 script: npm run native test:e2e:android:local ${{ matrix.native-test-name }} - - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: always() with: name: android-screen-recordings path: packages/react-native-editor/android-screen-recordings - - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: always() with: name: appium-logs diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index 9ead788343b8dc..220874d7649195 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -37,7 +37,7 @@ jobs: uses: ./.github/setup-node - name: Restore tests setup cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 with: path: | ~/.appium @@ -50,7 +50,7 @@ jobs: run: find package-lock.json packages/react-native-editor/ios packages/react-native-aztec/ios packages/react-native-bridge/ios -type f -print0 | sort -z | xargs -0 shasum | tee ios-checksums.txt - name: Restore build cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 with: path: | packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app @@ -58,7 +58,7 @@ jobs: key: ${{ runner.os }}-ios-build-${{ matrix.xcode }}-${{ matrix.device }}-${{ hashFiles('ios-checksums.txt') }} - name: Restore pods cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 with: path: | packages/react-native-editor/ios/Pods @@ -84,13 +84,13 @@ jobs: rm packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle rm -rf packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/assets - - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: always() with: name: ios-screen-recordings path: packages/react-native-editor/ios-screen-recordings - - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + - uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 if: always() with: name: appium-logs diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b6d5465ab43a6c..523418a79ef49a 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -125,7 +125,7 @@ jobs: # dependency versions are installed and cached. ## - name: Set up PHP - uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2.28.0 + uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d # v2.29.0 with: php-version: '${{ matrix.php }}' ini-file: development @@ -225,7 +225,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Set up PHP - uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2.28.0 + uses: shivammathur/setup-php@6d7209f44a25a59e904b1ee9f3b0c33ab2cd888d # v2.29.0 with: php-version: '7.4' coverage: none @@ -238,7 +238,7 @@ jobs: run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> $GITHUB_OUTPUT - name: Cache PHPCS scan cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 # v4.0.0 with: path: .cache/phpcs.json key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-phpcs-cache-${{ hashFiles('**/composer.json', 'phpcs.xml.dist') }} diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index d6d5f8da1102bb..e71bade9d0cd87 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -147,7 +147,7 @@ jobs: fi - name: Upload Changelog artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: name: changelog ${{ matrix.label }} path: ./changelog.txt @@ -189,7 +189,7 @@ jobs: sed -i "s/$STABLE_TAG_PLACEHOLDER/Stable tag: $VERSION/g" ./trunk/readme.txt - name: Download Changelog Artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: changelog trunk path: trunk @@ -247,7 +247,7 @@ jobs: sed -i "s/$STABLE_TAG_PLACEHOLDER/Stable tag: $VERSION/g" "$VERSION/readme.txt" - name: Download Changelog Artifact - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: changelog trunk path: ${{ github.event.release.name }} diff --git a/bin/packages/check-build-type-declaration-files.js b/bin/packages/check-build-type-declaration-files.js index fff0b51e32fc24..74c7cfa5e3c2ac 100644 --- a/bin/packages/check-build-type-declaration-files.js +++ b/bin/packages/check-build-type-declaration-files.js @@ -69,13 +69,16 @@ async function getDecFile( packagePath ) { async function typecheckDeclarations( file ) { return new Promise( ( resolve, reject ) => { - exec( `npx tsc --noEmit ${ file }`, ( error, stdout, stderr ) => { - if ( error ) { - reject( { file, error, stderr, stdout } ); - } else { - resolve( { file, stdout } ); + exec( + `npx tsc --target esnext --moduleResolution node --noEmit ${ file }`, + ( error, stdout, stderr ) => { + if ( error ) { + reject( { file, error, stderr, stdout } ); + } else { + resolve( { file, stdout } ); + } } - } ); + ); } ); } diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md index ce9eb458e73723..a385c1ced9e43c 100644 --- a/docs/contributors/versions-in-wordpress.md +++ b/docs/contributors/versions-in-wordpress.md @@ -6,6 +6,7 @@ If anything looks incorrect here, please bring it up in #core-editor in [WordPre | Gutenberg Versions | WordPress Version | | ------------------ | ----------------- | +| 16.2-16.7 | 6.4.3 | | 16.2-16.7 | 6.4.2 | | 16.2-16.7 | 6.4.1 | | 16.2-16.7 | 6.4 | diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index b45a740c2a58cd..f1b5999ae31472 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -1,44 +1,37 @@ # Getting Started -Welcome! Let's get started building with blocks. Blocks are at the core of extending WordPress. You can create custom blocks, your own block patterns, or combine them together to build a block theme. +Welcome to the Getting Started documentation. From setting up your development environment and building your first block to understanding the fundamentals, this section is the perfect starting point if you are new to block development or want to improve your skills. ## Navigating this chapter -For those starting with block development, this section is the perfect starting point as it provides the knowledge you need to start creating your own custom blocks. +Use the following links to locate a topic within this chapter. If you have never built a block before, consider reading through the documentation in the order listed. -- [**Block Development Environment**](https://developer.wordpress.org/block-editor/getting-started/devenv/) - Set up the right development environment to create blocks and get introduced to basic tools for block development such as [`wp-env`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/), [`create-block`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) and [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) -- [**Quick Start Guide**](https://developer.wordpress.org/block-editor/getting-started/quick-start-guide/) - Get a block up and running in less than 1 min. -- [**Tutorial: Build your first block**](https://developer.wordpress.org/block-editor/getting-started/tutorial/) - The tutorial will guide you, step by step, through the complete process of creating a fully functional custom block. -- [**Fundamentals of Block Development**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/) - This section provides an introduction to the most relevant concepts in Block Development. -- [**Glossary**](https://developer.wordpress.org/block-editor/getting-started/glossary/) - Glossary of terms related to the Block Editor and Full Site Editing -- [**Frequently Asked Questions**](https://developer.wordpress.org/block-editor/getting-started/faq/) - Set of questions (and answers) that have come up from the last few years of Gutenberg development. +- **[Block Development Environment](https://developer.wordpress.org/block-editor/getting-started/devenv/):** Set up the right development environment to create blocks and get introduced to basic tools for block development such as [`wp-env`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/), [`create-block`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/), and [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) +- **[Quick Start Guide](https://developer.wordpress.org/block-editor/getting-started/quick-start-guide/):** Get a custom block up and running in less than one minute. +- **[Tutorial: Build your first block](https://developer.wordpress.org/block-editor/getting-started/tutorial/):** Learn how to build a fully functional custom block from the ground up. +- **[Fundamentals of Block Development](https://developer.wordpress.org/block-editor/getting-started/fundamentals/):** Learn the most relevant concepts in block development. +- **[Glossary](https://developer.wordpress.org/block-editor/getting-started/glossary/):** A glossary of common terms you will encounter when working with the Block Editor. +- **[Frequently Asked Questions](https://developer.wordpress.org/block-editor/getting-started/faq/):** Common questions (and answers) that have come up from the last few years of Gutenberg's development. -## Getting Started on the WordPress project and Gutenberg +## Keeping up with the WordPress project -At a high level, here are a few ways to begin your journey but read on to explore more: +Once you have finished reviewing this chapter, you will have a solid understanding of blocks and how to develop for the Block Editor, but what's next? -- Learn more about where this work is going by [reviewing the long term roadmap](https://wordpress.org/about/roadmap/). -- Explore the [GitHub repo](https://github.com/WordPress/gutenberg/) to see the latest issues and PRs folks are working on, especially [Good First Issues](https://github.com/WordPress/gutenberg/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22). -- Join the [Slack community](https://make.wordpress.org/chat/) to join meetings, ongoing conversations, and more. -- Take courses on how to use the block editor and more on [Learn WordPress](https://learn.wordpress.org/). -- Expand your knowledge by reviewing more developer docs at the overall [developer.wordpress.org resource](https://developer.wordpress.org/). -- Subscribe to [updates on Make Core](https://make.wordpress.org/core/), the main site where ongoing project updates happen. +The WordPress project, and Gutenberg in particular, iterates quickly. Staying up-to-date on all the changes can be challenging. So, here are a few essential developer resources you should be aware of. Each person will have their own unique needs in keeping up with a project of this scale, so choose what's right for you. -### Ways to Stay Informed +- **[WordPress Roadmap](https://wordpress.org/about/roadmap/):** The high-level roadmap for WordPress and Gutenberg. +- **[Make Core](https://make.wordpress.org/core/):** The primary blog for WordPress Core where all major project updates are posted. +- **[WordPress Slack](https://make.wordpress.org/chat/):** The official Slack community for all WordPress contributors is the hub for team meetings, ongoing conversations, and more. Make sure to join the `#core` and `#core-editor` channels. +- **[Gutenberg GitHub repository](https://github.com/WordPress/gutenberg/):** This is where all Block Editor development happens. Keeping a close eye on the repository will give you a real-time understanding of what’s being worked on by fellow contributors. +- **[Keeping up with Gutenberg](https://make.wordpress.org/core/handbook/references/keeping-up-with-gutenberg-index/):** A compilation of Gutenberg-related posts from the many [Make teams](https://make.wordpress.org/), including Core, Design, Meta, and Themes. +- **["What's new in Gutenberg?"](https://make.wordpress.org/core/tag/gutenberg-new/):** Biweekly posts published on Make Core with each Gutenberg release. They are a great way to review the most relevant new features and the full changelog. +- **["What's new for developers?"](https://developer.wordpress.org/news/):** Monthly posts on the WordPress Developer Blog that showcase the most important developer-related changes that happened in WordPress the previous month. -New features and changes are important to keep up to date on as the Gutenberg project continues. Each person will have their own unique needs in keeping up with a project of this scale. What follows is more of a catalogue of ways to keep up rather than a recommendation for how to do so. +## Additional resources -- [Keeping up with Gutenberg](https://make.wordpress.org/core/handbook/references/keeping-up-with-gutenberg-index/) - compilation of Gutenberg-related team posts of Core, Core-Editor, Core-js, Core-css, Design, Meta, and Themes, and other teams. -- [“What’s New In Gutenberg?” release posts](https://make.wordpress.org/core/tag/gutenberg-new/). These updates are wrangled by the Core Editor team and focus on what’s been released in each biweekly Gutenberg release. They include the most relevant features released and a full changelog. -- [Core Editor meetings](https://make.wordpress.org/core/tag/core-editor-summary/). These meetings are wrangled by volunteer members in the #core-editor Slack channel. [Agendas](https://make.wordpress.org/core/tag/core-editor-summary/) and [summaries](https://make.wordpress.org/core/tag/core-editor-summary/) are shared on the [Make Core blog](https://make.wordpress.org/core/). They focus on task coordination and relevant discussions around Gutenberg releases. There is an Open Floor period in each chat where people can suggest topics to discuss. -- Checking in on [issues](https://github.com/WordPress/gutenberg/issues) and [PRs](https://github.com/WordPress/gutenberg/pulls) on GitHub. This will give you a nearly real-time understanding of what’s being worked on by the developers and designers. +For more resources on block development and extending the Block Editor, review the additional sections here in the Block Editor Handbook. Further practical examples are also available in the [block-development-examples](https://github.com/wptrainingteam/block-development-examples) GitHub repository. - -## Additional Resources - -The [block-development-examples](https://github.com/wptrainingteam/block-development-examples) repo is the central hub of examples for block development referenced from this handbook. - -At [Learn WordPress](https://learn.wordpress.org/), you can find [tutorials](https://learn.wordpress.org/tutorials/), [courses](https://learn.wordpress.org/courses/), and [online workshops](https://learn.wordpress.org/online-workshops/) to learn more about developing for the Block Editor. Here is a selection of current offerings: +If you are looking for more educational content, check out [Learn WordPress](https://learn.wordpress.org/), where you can find [tutorials](https://learn.wordpress.org/tutorials/), [courses](https://learn.wordpress.org/courses/), and [online workshops](https://learn.wordpress.org/online-workshops/). Here is a selection of current offerings: - [Intro to Block Development: Build Your First Custom Block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/) - [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/) diff --git a/docs/getting-started/devenv/README.md b/docs/getting-started/devenv/README.md index b9acda48eab7fb..1c2b9ed9070d30 100644 --- a/docs/getting-started/devenv/README.md +++ b/docs/getting-started/devenv/README.md @@ -2,17 +2,19 @@ This guide will help you set up the right development environment to create blocks and other plugins that extend and modify the Block Editor in WordPress. -To contribute to the Gutenberg project itself, refer to the additional documentation in the [code contribution guide](/docs/contributors/code/getting-started-with-code-contribution.md). - A block development environment includes the tools you need on your computer to successfully develop for the Block Editor. The three essential requirements are: 1. [Code editor](#code-editor) 2. [Node.js development tools](#node-js-development-tools) 3. [Local WordPress environment (site)](#local-wordpress-environment) +
+ To contribute to the Gutenberg project itself, refer to the additional documentation in the code contribution guide. +
+ ## Code editor -A code editor is used to write code, and you can use whichever editor you're most comfortable with. The key is having a way to open, edit, and save text files. +A code editor is used to write code. You can use whichever editor you're most comfortable with. The key is having a way to open, edit, and save text files. If you do not already have a preferred code editor, [Visual Studio Code](https://code.visualstudio.com/) (VS Code) is a popular choice for JavaScript development among Core contributors. It works well across the three major platforms (Windows, Linux, and Mac), is open-source, and is actively maintained by Microsoft. VS Code also has a vibrant community providing plugins and extensions, including many for WordPress development. @@ -30,7 +32,7 @@ Node.js and its accompanying development tools allow you to: The list goes on. While modern JavaScript development can be challenging, WordPress provides several tools, like [`wp-scripts`](/docs/getting-started/devenv/get-started-with-wp-scripts.md) and [`create-block`](/docs/getting-started/devenv/get-started-with-create-block.md), that streamline the process and are made possible by Node.js development tools. -**The recommended Node.js version for block development is [Active LTS](https://nodejs.org/en/about/previous-releases) (Long Term Support)**. However, there are times when you need to use different versions. A Node.js version manager tool like `nvm` is strongly recommended and allows you to easily change your `node` version when required. You will also need Node Package Manager (`npm`) and the Node Package eXecute (`npx`) to work with some WordPress packages. Both are installed automatically with Node.js. +**The recommended Node.js version for block development is [Active LTS](https://nodejs.org/en/about/previous-releases) (Long Term Support)**. However, there are times when you need to use different versions. A Node.js version manager tool like `nvm` is strongly recommended and allows you to change your `node` version when required. You will also need Node Package Manager (`npm`) and the Node Package eXecute (`npx`) to work with some WordPress packages. Both are installed automatically with Node.js. To be able to use the Node.js tools and [packages provided by WordPress](https://github.com/WordPress/gutenberg/tree/trunk/packages) for block development, you'll need to set a proper Node.js runtime environment on your machine. To learn more about how to do this, refer to the links below. @@ -39,9 +41,9 @@ To be able to use the Node.js tools and [packages provided by WordPress](https:/ ## Local WordPress environment -A local WordPress environment (site) provides a controlled, efficient, and secure space for development, allowing you to build and test your code before deploying it to a production site. The [same requirements](https://en-gb.wordpress.org/about/requirements/) for WordPress apply to local sites. +A local WordPress environment (site) provides a controlled, efficient, and secure space for development, allowing you to build and test your code before deploying it to a production site. The same [requirements](https://en-gb.wordpress.org/about/requirements/) for WordPress apply to local sites. -In the broader WordPress community, there are many available tools for setting up a local WordPress environment on your computer. The Block Editor Handbook covers `wp-env`, which is open-source and maintained by the WordPress project itself. It's also the recommended tool for Gutenberg development. +In the broader WordPress community, many tools are available for setting up a local WordPress environment on your computer. The Block Editor Handbook covers `wp-env`, which is open-source and maintained by the WordPress project itself. It's also the recommended tool for Gutenberg development. Refer to the [Get started with `wp-env`](/docs/getting-started/devenv/get-started-with-wp-env.md) guide for setup instructions. diff --git a/docs/getting-started/fundamentals/README.md b/docs/getting-started/fundamentals/README.md index 0683e55d7edf76..fd2941711dd01b 100644 --- a/docs/getting-started/fundamentals/README.md +++ b/docs/getting-started/fundamentals/README.md @@ -1,14 +1,12 @@ # Fundamentals of Block Development -This section provides an introduction to the most relevant concepts in Block Development. +This section provides an introduction to the most relevant concepts in block development. Use the following links to learn more: -In this section, you will learn: - -1. [**File structure of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block) - The purpose of each one of the types of files available for a block, the relationships between them, and their role in the output of the block. -1. [**`block.json`**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json) - How a block is defined using its `block.json` metadata and some relevant properties of this file (such as `attributes` and `supports`). -1. [**Registration of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How a block is registered in both the server and the client. -1. [**Block wrapper**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper) - How to set proper attributes to the block's markup wrapper. -1. [**The block in the Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-in-the-editor) - The block as a React component loaded in the Block Editor and its possibilities. -1. [**Markup representation of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block) - How blocks are represented in the database, theme templates, or patterns. -1. [**Static or Dynamic rendering of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/static-dynamic-rendering) - How blocks can generate their output for the front end dynamically or statically. -1. [**Javascript in the Block Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor) - How to work with Javascript for the Block Editor. \ No newline at end of file +1. **[File structure of a block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block):** The purpose of each file that composes a block plugin, the relationships between them, and their role in the block output. +1. **[`block.json`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json):** How a block is defined using its `block.json` metadata and some relevant properties of this file (such as `attributes` and `supports`). +1. **[Registration of a block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block):** How a block is registered on both the server and in the client. +1. **[Block wrapper](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper):** How to apply the proper attributes to the block's markup wrapper. +1. **[The block in the Editor](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-in-the-editor):** How a block, as a React component, is loaded in the Block Editor and an overview of its structure. +1. **[Markup representation of a block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block):** How blocks are represented in the database, theme templates, and patterns. +1. **[Static or Dynamic rendering of a block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/static-dynamic-rendering):** How blocks generate their front-end output either dynamically or statically. +1. **[Javascript in the Block Editor](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor):** How to work with modern Javascript when developing for the Block Editor. diff --git a/docs/getting-started/fundamentals/file-structure-of-a-block.md b/docs/getting-started/fundamentals/file-structure-of-a-block.md index b52f5c5efd0f76..eb3f38e0be5c4f 100644 --- a/docs/getting-started/fundamentals/file-structure-of-a-block.md +++ b/docs/getting-started/fundamentals/file-structure-of-a-block.md @@ -1,86 +1,90 @@ # File structure of a block -It is recommended to **register blocks within plugins** to ensure they stay available when a theme gets switched. With the [`create-block` tool](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) you can quickly scaffold the structure of the files required to create a plugin that registers a block. +When developing custom blocks for WordPress, it's best practice to register them within plugins rather than themes. This strategy guarantees that your blocks stay accessible, even when users switch themes. While there might be situations where embedding blocks directly into a theme could be appropriate, this guide focuses on blocks housed within a plugin. Specifically, it details the file structure as produced by the [`create-block`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) tool. -The files generated by `create-block` are a good reference of the files that can be involved in the definition and registration of a block. +Adhering to the `create-block` tool's structure is not mandatory, but it serves as a reliable reference. The files it generates encompass everything needed for a block's definition and registration. Following this structure can help maintain consistency and ensure your blocks are well-organized and easy to maintain. [![Open File Structure of a Block diagram image](https://developer.wordpress.org/files/2023/11/file-structure-block.png)](https://developer.wordpress.org/files/2023/11/file-structure-block.png "Open File Structure of a Block diagram image") -## Folders and files involved in a block's definition and registration +## `.php` -### `.php` -A block is usually added to the block editor using a WordPress plugin. In the main PHP file of the plugin the block is usually registered on the server side. +When creating a block in a WordPress plugin, you usually register the block on the server in the main PHP file of the plugin. This is done using the [`register_block_type()`](https://developer.wordpress.org/reference/functions/register_block_type/) function.
-For more on creating a WordPress plugin see Plugin Basics, and Plugin Header requirements for explanation and additional fields you can include in your plugin header. + For more on creating a WordPress plugin, refer to the documentation on Plugin Basics and the Header Requirements for the main PHP file.
-### `package.json` +## `package.json` -[`package.json`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) is a configuration file for a Node.js project. In this file you define the NPM dependencies of the block and the scripts used for local work. +The `package.json` file is used to configure a Node.js project, which is technically what a block plugin is. In this file, you define the `npm` dependencies of the block and the scripts used for local development. -### `src` folder +## `src` folder -In a standard project you'll place your block files in the `src` folder. By default, [the build process with `wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor/#javascript-build-process) will take files from this folder and will generate the bundled files in the `build` folder. +In a standard project, the `src` (source) folder contains the raw, uncompiled code, including JavaScript, CSS, and other assets necessary for developing the block. This is where you write and edit your block's source code, utilizing modern JavaScript features and JSX for React components. + +The [build process](docs/block-editor/getting-started/fundamentals/javascript-in-the-block-editor/#javascript-build-process.md) provided by `wp-scripts` will then take the files from this folder and generate the production-ready files in the project's `build` folder. ### `block.json` -This file contains the [metadata of the block](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/), and it's used to simplify the definition and registration of the block both in the client and on the server. +The `block.json` file contains the [block's metadata](docs/block-editor/reference-guides/block-api/block-metadata/), streamlining its definition and registration across client-side and server-side environments. + +This file includes the block name, description, [attributes](docs/block-editor/reference-guides/block-api/block-attributes.md), [supports](docs/block-editor/reference-guides/block-api/block-supports.md), and more, as well as the locations of essential files responsible for the block's functionality, appearance, and styling. -Among other data it provides properties to define the paths of the files involved in the block's behaviour, output and style. If there's a build process involved, this `block.json` along with the generated files are placed into a destination folder (usually the `build` folder) so the paths provided target to the bundled versions of these files. +When a build process is applied, the `block.json` file and the other generated files are moved to a designated folder, often the `build` folder. Consequently, the file paths specified within `block.json` point to these processed, bundled versions of the files. -The most relevant properties that can be defined in a `block.json` to set the files involved in the block's behaviour, output, or style are: +A few of the most important properties that can be defined in a `block.json` are: -- The [`editorScript`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-script) property, usually set with the path of a bundled `index.js` file (output build from `src/index.js`). -- The [`style`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#style) property, usually set with the path of a bundled `style-index.css` file (output build from `src/style.(css|scss|sass)`). -- The [`editorStyle`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-style) property, usually set with the path of a bundled `index.css` (output build from `src/editor.(css|scss|sass)`). -- The [`render`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#render) property, usually set with the path of a bundled `render.php` (output copied from `src/render.php`). -- The [`viewScript`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script) property, usually set with the path of a bundled `view.js` (output copied from `src/view.php`). +- **[`editorScript`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-script):** Usually set with the path of a bundled `index.js` file that was built from `src/index.js`. +- **[`style`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#style):** Usually set with the path of a bundled `style-index.css` file that was built from `src/style.(css|scss|sass)`. +- **[`editorStyle`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-style):** Usually set with the path of a bundled `index.css` that was built from `src/editor.(css|scss|sass)`. +- **[`render`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#render):** Usually set with the path of a bundled `render.php` that was copied from `src/render.php`. +- **[`viewScript`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script):** Usually set with the path of a bundled `view.js` that was built from `src/view.js`. [![Open Build Output Diagram in excalidraw](https://developer.wordpress.org/files/2023/11/file-structure-build-output.png)](https://excalidraw.com/#json=c22LROgcG4JkD-7SkuE-N,rQW_ViJBq0Yk3qhCgqD6zQ "Open Build Output Diagram in excalidraw") ### `index.js` -The `index.js` file (or any other file defined in the `editorScript` property of `block.json`) is the entry point file for javascript that should only get loaded in the editor. It is responsible for calling the `registerBlockType` function to register the block on the client. In a standard structure it imports the `edit.js` and `save.js` files to get functions required in block registration. +The `index.js` file (or any other file defined in the `editorScript` property of `block.json`) is the entry point file for JavaScript that should only get loaded in the Block Editor. It's responsible for calling the `registerBlockType` function to register the block on the client and typically imports the `edit.js` and `save.js` files to get the functions required for block registration. ### `edit.js` -The `edit.js` commonly gets used to contain the React component that gets used in the editor for our block. It usually exports a single component that then gets passed to the [`edit` property of the `registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit) function in the `index.js` file. +The `edit.js` file contains the React component responsible for rendering the block's editing user interface, allowing users to interact with and customize the block's content and settings in the Block Editor. This component gets passed to the [`edit`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit) property of the `registerBlockType` function in the `index.js` file. ### `save.js` -The `save.js` exports the function that returns the static HTML markup that gets saved to the Database and that is passed to the [`save` property of the `registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save) function in the `index.js` file. +The `save.js` exports the function that returns the static HTML markup that gets saved to the WordPress database. This function gets passed to the [`save`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save) property of the `registerBlockType` function in the `index.js` file. ### `style.(css|scss|sass)` -A `style` file with any of the extensions `.css`, `.scss` or `.sass`, contains the styles of the block that will be loaded in both the editor and the frontend. In the build process this file is converted into `style-index.css` which is usually defined at [`style`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#style) property in `block.json` +A `style` file with extensions `.css`, `.scss`, or `.sass` contains the styles of the block that will be loaded in both the Block Editor and on the front end. In the build process, this file is converted into `style-index.css`, which is usually defined using the [`style`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#style) property in `block.json`
- The webpack config used internally by wp-scripts includes a css-loader chained with postcss-loader and sass-loader that allows it to process CSS, SASS or SCSS files. Check Default webpack config for more info + The webpack configuration used internally by wp-scripts includes a css-loader chained with postcss-loader and sass-loader that allows it to process CSS, SASS or SCSS files. Check Default webpack config for more info
- ### `editor.(css|scss|sass)` -An `editor` file with any of the extensions `.css`, `.scss` or `.sass`, contains the additional styles applied to the block only in the editor’s context. In the build process this file is converted into `index.css` which is usually defined at [`editorStyle`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#editor-style) property in `block.json` +An `editor` file with extensions `.css`, `.scss`, or `.sass` contains the additional styles applied to the block in the Block Editor. You will often use this file for styles specific to the block's user interface. This file is converted to `index.css` during the build process, usually defined using the `editorStyle` property in `block.json`. ### `render.php` -The `render.php` file (or any other file defined in the [`render`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#render) property of `block.json`) defines the server side process that returns the markup for the block when there is a request from the frontend. If this file is defined, it will take precedence over any other ways to render the block's markup for the frontend. +The `render.php` file (or any other file defined in the [`render`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#render) property of `block.json`) defines the server-side process that returns the markup for the block when there is a request from the front end. If defined, this file will take precedence over other ways to render the block's markup on the front end. ### `view.js` -The `view.js` file (or any other file defined in the [`viewScript`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script) property of `block.json`) will be loaded in the front-end when the block is displayed. +The `view.js` file (or any other file defined in the [`viewScript`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#view-script) property of `block.json`) will be loaded in the front end when the block is displayed. -### `build` folder +## `build` folder -In a standard project, the `build` folder contains the generated files in [the build process triggered by the `build` or `start` commands of `wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/#the-build-process-with-wp-scripts). +The `build` folder contains the compiled and optimized versions of the code from the `src` folder. These files are generated from the [build process](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/#the-build-process-with-wp-scripts), triggered by the `build` or `start` commands of `wp-scripts`. -
- You can use webpack-src-dir and output-path option of wp-scripts build commands to customize the entry and output points +This transformation process includes minification, transpilation from modern JavaScript to a version compatible with a wider range of browsers, and bundling of assets for efficient loading. WordPress ultimately enqueues and uses the `build` folder's contents to render the block in the Block Editor and on the front end. + +
+ You can use webpack-src-dir and output-path option of wp-scripts build commands to customize the entry and output points.
## Additional resources -- [File Structure of a Block diagram](https://excalidraw.com/#json=YYpeR-kY1ZMhFKVZxGhMi,mVZewfwNAh_oL-7bj4gmdw) +- [Diagram featuring the file structure of a block](https://excalidraw.com/#json=YYpeR-kY1ZMhFKVZxGhMi,mVZewfwNAh_oL-7bj4gmdw) diff --git a/docs/how-to-guides/propagating-updates.md b/docs/how-to-guides/propagating-updates.md index 5f2861a4e456c6..8079048a47a89c 100644 --- a/docs/how-to-guides/propagating-updates.md +++ b/docs/how-to-guides/propagating-updates.md @@ -36,9 +36,9 @@ If needed, one potential workaround for patterns with custom styles is to add a It is not fool-proof because users can modify the class via the editor UI. However, because this setting is under the "Advanced" panel it is likely to stay intact in most instances. This gives theme authors some CSS control for some pattern types, allowing them to update existing uses. However, it does not prevent users from making massive alterations that cannot be updated. -### Reusable blocks +### Synced Patterns -As the name suggests, these blocks are inherently synced across your site. Keep in mind that there are currently limitations with relying on reusable blocks to handle certain updates since content, HTML structure, and styles will all stay in sync when updates happen. If you require more nuance than that, this is a key element to factor in and a dynamic block might be a better approach. +As the name suggests, these patterns are inherently synced across your site. Keep in mind that there are currently limitations with relying on synced patterns to handle certain updates since content, HTML structure, and styles will all stay in sync when updates happen. If you require more nuance than that, this is a key element to factor in and a dynamic block might be a better approach. ### Template parts and templates diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 0708ee342593e3..35cecf6a27e663 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -14,7 +14,7 @@ Display a date archive of your posts. ([Source](https://github.com/WordPress/gut - **Name:** core/archives - **Category:** widgets -- **Supports:** align, spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** displayAsDropdown, showLabel, showPostCounts, type ## Audio @@ -23,7 +23,7 @@ Embed a simple audio player. ([Source](https://github.com/WordPress/gutenberg/tr - **Name:** core/audio - **Category:** media -- **Supports:** align, anchor, spacing (margin, padding) +- **Supports:** align, anchor, interactivity (clientNavigation), spacing (margin, padding) - **Attributes:** autoplay, caption, id, loop, preload, src ## Avatar @@ -32,7 +32,7 @@ Add a user’s avatar. ([Source](https://github.com/WordPress/gutenberg/tree/tru - **Name:** core/avatar - **Category:** theme -- **Supports:** align, color (~~background~~, ~~text~~), spacing (margin, padding), ~~alignWide~~, ~~html~~ +- **Supports:** align, color (~~background~~, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), ~~alignWide~~, ~~html~~ - **Attributes:** isLink, linkTarget, size, userId ## Pattern @@ -41,7 +41,7 @@ Reuse this design across your site. ([Source](https://github.com/WordPress/guten - **Name:** core/block - **Category:** reusable -- **Supports:** ~~customClassName~~, ~~html~~, ~~inserter~~, ~~renaming~~ +- **Supports:** interactivity (clientNavigation), ~~customClassName~~, ~~html~~, ~~inserter~~, ~~renaming~~ - **Attributes:** overrides, ref ## Button @@ -51,7 +51,7 @@ Prompt visitors to take action with a button-style link. ([Source](https://githu - **Name:** core/button - **Category:** design - **Parent:** core/buttons -- **Supports:** anchor, color (background, gradients, text), shadow, spacing (padding), typography (fontSize, lineHeight), ~~alignWide~~, ~~align~~, ~~reusable~~ +- **Supports:** anchor, color (background, gradients, text), interactivity (clientNavigation), shadow, spacing (padding), typography (fontSize, lineHeight), ~~alignWide~~, ~~align~~, ~~reusable~~ - **Attributes:** backgroundColor, gradient, linkTarget, placeholder, rel, tagName, text, textAlign, textColor, title, type, url, width ## Buttons @@ -60,7 +60,7 @@ Prompt visitors to take action with a group of button-style links. ([Source](htt - **Name:** core/buttons - **Category:** design -- **Supports:** align (full, wide), anchor, layout (default, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), anchor, interactivity (clientNavigation), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** ## Calendar @@ -69,7 +69,7 @@ A calendar of your site’s posts. ([Source](https://github.com/WordPress/gutenb - **Name:** core/calendar - **Category:** widgets -- **Supports:** align, color (background, link, text), typography (fontSize, lineHeight) +- **Supports:** align, color (background, link, text), interactivity (clientNavigation), typography (fontSize, lineHeight) - **Attributes:** month, year ## Categories List @@ -78,7 +78,7 @@ Display a list of all categories. ([Source](https://github.com/WordPress/gutenbe - **Name:** core/categories - **Category:** widgets -- **Supports:** align, spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** displayAsDropdown, showEmpty, showHierarchy, showOnlyTopLevel, showPostCounts ## Code @@ -87,7 +87,7 @@ Display code snippets that respect your spacing and tabs. ([Source](https://gith - **Name:** core/code - **Category:** text -- **Supports:** align (wide), anchor, color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** align (wide), anchor, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** content ## Column @@ -97,7 +97,7 @@ A single column within a columns block. ([Source](https://github.com/WordPress/g - **Name:** core/column - **Category:** design - **Parent:** core/columns -- **Supports:** anchor, color (background, button, gradients, heading, link, text), layout, spacing (blockGap, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** anchor, color (background, button, gradients, heading, link, text), interactivity (clientNavigation), layout, spacing (blockGap, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** allowedBlocks, templateLock, verticalAlignment, width ## Columns @@ -106,7 +106,7 @@ Display content in multiple columns, with blocks added to each column. ([Source] - **Name:** core/columns - **Category:** design -- **Supports:** align (full, wide), anchor, color (background, button, gradients, heading, link, text), layout (default, ~~allowEditing~~, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), anchor, color (background, button, gradients, heading, link, text), interactivity (clientNavigation), layout (default, ~~allowEditing~~, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** isStackedOnMobile, templateLock, verticalAlignment ## Comment Author Avatar (deprecated) @@ -116,7 +116,7 @@ This block is deprecated. Please use the Avatar block instead. ([Source](https:/ - **Name:** core/comment-author-avatar - **Experimental:** fse - **Category:** theme -- **Supports:** color (background, ~~text~~), spacing (margin, padding), ~~html~~, ~~inserter~~ +- **Supports:** color (background, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), ~~html~~, ~~inserter~~ - **Attributes:** height, width ## Comment Author Name @@ -125,7 +125,7 @@ Displays the name of the author of the comment. ([Source](https://github.com/Wor - **Name:** core/comment-author-name - **Category:** theme -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** isLink, linkTarget, textAlign ## Comment Content @@ -143,7 +143,7 @@ Displays the date on which the comment was posted. ([Source](https://github.com/ - **Name:** core/comment-date - **Category:** theme -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** format, isLink ## Comment Edit Link @@ -152,7 +152,7 @@ Displays a link to edit the comment in the WordPress Dashboard. This link is onl - **Name:** core/comment-edit-link - **Category:** theme -- **Supports:** color (background, gradients, link, ~~text~~), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** linkTarget, textAlign ## Comment Reply Link @@ -171,7 +171,7 @@ Contains the block elements used to display a comment, like the title, date, aut - **Name:** core/comment-template - **Category:** design - **Parent:** core/comments -- **Supports:** align, spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** align, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** ## Comments @@ -190,7 +190,7 @@ Displays a paginated navigation to next/previous set of comments, when applicabl - **Name:** core/comments-pagination - **Category:** theme - **Parent:** core/comments -- **Supports:** align, color (background, gradients, link, text), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** align, color (background, gradients, link, text), interactivity (clientNavigation), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** paginationArrow ## Comments Next Page @@ -200,7 +200,7 @@ Displays the next comment's page link. ([Source](https://github.com/WordPress/gu - **Name:** core/comments-pagination-next - **Category:** theme - **Parent:** core/comments-pagination -- **Supports:** color (background, gradients, ~~text~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** color (background, gradients, ~~text~~), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** label ## Comments Page Numbers @@ -210,7 +210,7 @@ Displays a list of page numbers for comments pagination. ([Source](https://githu - **Name:** core/comments-pagination-numbers - **Category:** theme - **Parent:** core/comments-pagination -- **Supports:** color (background, gradients, ~~text~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** color (background, gradients, ~~text~~), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** ## Comments Previous Page @@ -220,7 +220,7 @@ Displays the previous comment's page link. ([Source](https://github.com/WordPres - **Name:** core/comments-pagination-previous - **Category:** theme - **Parent:** core/comments-pagination -- **Supports:** color (background, gradients, ~~text~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** color (background, gradients, ~~text~~), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** label ## Comments Title @@ -229,7 +229,7 @@ Displays a title with the number of comments. ([Source](https://github.com/WordP - **Name:** core/comments-title - **Category:** theme -- **Supports:** align, color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~anchor~~, ~~html~~ +- **Supports:** align, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~anchor~~, ~~html~~ - **Attributes:** level, showCommentsCount, showPostTitle, textAlign ## Cover @@ -238,7 +238,7 @@ Add an image or video with a text overlay. ([Source](https://github.com/WordPres - **Name:** core/cover - **Category:** media -- **Supports:** align, anchor, color (heading, text, ~~background~~, ~~enableContrastChecker~~), dimensions (aspectRatio), layout (~~allowJustification~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align, anchor, color (heading, text, ~~background~~, ~~enableContrastChecker~~), dimensions (aspectRatio), interactivity (clientNavigation), layout (~~allowJustification~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** allowedBlocks, alt, backgroundType, contentPosition, customGradient, customOverlayColor, dimRatio, focalPoint, gradient, hasParallax, id, isDark, isRepeated, isUserOverlayColor, minHeight, minHeightUnit, overlayColor, tagName, templateLock, url, useFeaturedImage ## Details @@ -247,7 +247,7 @@ Hide and show additional content. ([Source](https://github.com/WordPress/gutenbe - **Name:** core/details - **Category:** text -- **Supports:** align (full, wide), color (background, gradients, link, text), layout (~~allowEditing~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), layout (~~allowEditing~~), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** showContent, summary ## Embed @@ -256,7 +256,7 @@ Add a block that displays content pulled from other sites, like Twitter or YouTu - **Name:** core/embed - **Category:** embed -- **Supports:** align, spacing (margin) +- **Supports:** align, interactivity (clientNavigation), spacing (margin) - **Attributes:** allowResponsive, caption, previewable, providerNameSlug, responsive, type, url ## File @@ -274,7 +274,7 @@ Display footnotes added to the page. ([Source](https://github.com/WordPress/gute - **Name:** core/footnotes - **Category:** text -- **Supports:** color (background, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~inserter~~, ~~multiple~~, ~~reusable~~ +- **Supports:** color (background, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~inserter~~, ~~multiple~~, ~~reusable~~ - **Attributes:** ## Form @@ -332,7 +332,7 @@ Display multiple images in a rich gallery. ([Source](https://github.com/WordPres - **Name:** core/gallery - **Category:** media -- **Supports:** align, anchor, color (background, gradients, ~~text~~), layout (default, ~~allowEditing~~, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin, padding), units (em, px, rem, vh, vw), ~~html~~ +- **Supports:** align, anchor, color (background, gradients, ~~text~~), interactivity (clientNavigation), layout (default, ~~allowEditing~~, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin, padding), units (em, px, rem, vh, vw), ~~html~~ - **Attributes:** allowResize, caption, columns, fixedHeight, ids, imageCrop, images, linkTarget, linkTo, randomOrder, shortCodeTransforms, sizeSlug ## Group @@ -341,7 +341,7 @@ Gather blocks in a layout container. ([Source](https://github.com/WordPress/gute - **Name:** core/group - **Category:** design -- **Supports:** align (full, wide), anchor, ariaLabel, background (backgroundImage, backgroundSize), color (background, button, gradients, heading, link, text), dimensions (minHeight), layout (allowSizingOnChildren), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), anchor, ariaLabel, background (backgroundImage, backgroundSize), color (background, button, gradients, heading, link, text), dimensions (minHeight), interactivity (clientNavigation), layout (allowSizingOnChildren), position (sticky), spacing (blockGap, margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** allowedBlocks, tagName, templateLock ## Heading @@ -350,7 +350,7 @@ Introduce new sections and organize content to help visitors (and search engines - **Name:** core/heading - **Category:** text -- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, className, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** __unstablePasteTextInline, align (full, wide), anchor, className, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** content, level, placeholder, textAlign ## Home Link @@ -360,7 +360,7 @@ Create a link that always points to the homepage of the site. Usually not necess - **Name:** core/home-link - **Category:** design - **Parent:** core/navigation -- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** label ## Custom HTML @@ -369,7 +369,7 @@ Add custom HTML code and preview it as you edit. ([Source](https://github.com/Wo - **Name:** core/html - **Category:** widgets -- **Supports:** ~~className~~, ~~customClassName~~, ~~html~~ +- **Supports:** interactivity (clientNavigation), ~~className~~, ~~customClassName~~, ~~html~~ - **Attributes:** content ## Image @@ -387,7 +387,7 @@ Display a list of your most recent comments. ([Source](https://github.com/WordPr - **Name:** core/latest-comments - **Category:** widgets -- **Supports:** align, spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** commentsToShow, displayAvatar, displayDate, displayExcerpt ## Latest Posts @@ -396,7 +396,7 @@ Display a list of your most recent posts. ([Source](https://github.com/WordPress - **Name:** core/latest-posts - **Category:** widgets -- **Supports:** align, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** addLinkToFeaturedImage, categories, columns, displayAuthor, displayFeaturedImage, displayPostContent, displayPostContentRadio, displayPostDate, excerptLength, featuredImageAlign, featuredImageSizeHeight, featuredImageSizeSlug, featuredImageSizeWidth, order, orderBy, postLayout, postsToShow, selectedAuthor ## List @@ -405,7 +405,7 @@ Create a bulleted or numbered list. ([Source](https://github.com/WordPress/guten - **Name:** core/list - **Category:** text -- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ +- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ - **Attributes:** ordered, placeholder, reversed, start, type, values ## List item @@ -415,7 +415,7 @@ Create a list item. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/ - **Name:** core/list-item - **Category:** text - **Parent:** core/list -- **Supports:** spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ +- **Supports:** interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ - **Attributes:** content, placeholder ## Login/out @@ -424,7 +424,7 @@ Show login & logout links. ([Source](https://github.com/WordPress/gutenberg/tree - **Name:** core/loginout - **Category:** theme -- **Supports:** className, spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** className, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** displayLoginAsForm, redirectToCurrent ## Media & Text @@ -433,7 +433,7 @@ Set media and words side-by-side for a richer layout. ([Source](https://github.c - **Name:** core/media-text - **Category:** media -- **Supports:** align (full, wide), anchor, color (background, gradients, heading, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), anchor, color (background, gradients, heading, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** align, allowedBlocks, focalPoint, href, imageFill, isStackedOnMobile, linkClass, linkDestination, linkTarget, mediaAlt, mediaId, mediaLink, mediaPosition, mediaSizeSlug, mediaType, mediaUrl, mediaWidth, rel, verticalAlignment ## Unsupported @@ -442,7 +442,7 @@ Your site doesn’t include support for this block. ([Source](https://github.com - **Name:** core/missing - **Category:** text -- **Supports:** ~~className~~, ~~customClassName~~, ~~html~~, ~~inserter~~, ~~reusable~~ +- **Supports:** interactivity (clientNavigation), ~~className~~, ~~customClassName~~, ~~html~~, ~~inserter~~, ~~reusable~~ - **Attributes:** originalContent, originalName, originalUndelimitedContent ## More @@ -451,7 +451,7 @@ Content before this block will be shown in the excerpt on your archives page. ([ - **Name:** core/more - **Category:** design -- **Supports:** ~~className~~, ~~customClassName~~, ~~html~~, ~~multiple~~ +- **Supports:** interactivity (clientNavigation), ~~className~~, ~~customClassName~~, ~~html~~, ~~multiple~~ - **Attributes:** customText, noTeaser ## Navigation @@ -470,7 +470,7 @@ Add a page, link, or another item to your navigation. ([Source](https://github.c - **Name:** core/navigation-link - **Category:** design - **Parent:** core/navigation -- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~renaming~~, ~~reusable~~ +- **Supports:** interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~renaming~~, ~~reusable~~ - **Attributes:** description, id, isTopLevelLink, kind, label, opensInNewTab, rel, title, type, url ## Submenu @@ -480,7 +480,7 @@ Add a submenu to your navigation. ([Source](https://github.com/WordPress/gutenbe - **Name:** core/navigation-submenu - **Category:** design - **Parent:** core/navigation -- **Supports:** ~~html~~, ~~reusable~~ +- **Supports:** interactivity (clientNavigation), ~~html~~, ~~reusable~~ - **Attributes:** description, id, isTopLevelItem, kind, label, opensInNewTab, rel, title, type, url ## Page Break @@ -490,7 +490,7 @@ Separate your content into a multi-page experience. ([Source](https://github.com - **Name:** core/nextpage - **Category:** design - **Parent:** core/post-content -- **Supports:** ~~className~~, ~~customClassName~~, ~~html~~ +- **Supports:** interactivity (clientNavigation), ~~className~~, ~~customClassName~~, ~~html~~ - **Attributes:** ## Page List @@ -499,7 +499,7 @@ Display a list of all pages. ([Source](https://github.com/WordPress/gutenberg/tr - **Name:** core/page-list - **Category:** widgets -- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** isNested, parentPageID ## Page List Item @@ -509,7 +509,7 @@ Displays a page inside a list of all pages. ([Source](https://github.com/WordPre - **Name:** core/page-list-item - **Category:** widgets - **Parent:** core/page-list -- **Supports:** ~~html~~, ~~inserter~~, ~~lock~~, ~~reusable~~ +- **Supports:** interactivity (clientNavigation), ~~html~~, ~~inserter~~, ~~lock~~, ~~reusable~~ - **Attributes:** hasChildren, id, label, link, title ## Paragraph @@ -518,7 +518,7 @@ Start with the basic building block of all narrative. ([Source](https://github.c - **Name:** core/paragraph - **Category:** text -- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ +- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~ - **Attributes:** align, content, direction, dropCap, placeholder ## Pattern placeholder @@ -527,7 +527,7 @@ Show a block pattern. ([Source](https://github.com/WordPress/gutenberg/tree/trun - **Name:** core/pattern - **Category:** theme -- **Supports:** ~~html~~, ~~inserter~~, ~~renaming~~ +- **Supports:** interactivity (clientNavigation), ~~html~~, ~~inserter~~, ~~renaming~~ - **Attributes:** slug ## Author @@ -536,7 +536,7 @@ Display post author details such as name, avatar, and bio. ([Source](https://git - **Name:** core/post-author - **Category:** theme -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** avatarSize, byline, isLink, linkTarget, showAvatar, showBio, textAlign ## Author Biography @@ -545,7 +545,7 @@ The author biography. ([Source](https://github.com/WordPress/gutenberg/tree/trun - **Name:** core/post-author-biography - **Category:** theme -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** textAlign ## Author Name @@ -554,7 +554,7 @@ The author name. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/pac - **Name:** core/post-author-name - **Category:** theme -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** isLink, linkTarget, textAlign ## Comment (deprecated) @@ -564,7 +564,7 @@ This block is deprecated. Please use the Comments block instead. ([Source](https - **Name:** core/post-comment - **Experimental:** fse - **Category:** theme -- **Supports:** ~~html~~, ~~inserter~~ +- **Supports:** interactivity (clientNavigation), ~~html~~, ~~inserter~~ - **Attributes:** commentId ## Comments Count @@ -574,7 +574,7 @@ Display a post's comments count. ([Source](https://github.com/WordPress/gutenber - **Name:** core/post-comments-count - **Experimental:** fse - **Category:** theme -- **Supports:** color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign ## Comments Form @@ -593,7 +593,7 @@ Displays the link to the current post comments. ([Source](https://github.com/Wor - **Name:** core/post-comments-link - **Experimental:** fse - **Category:** theme -- **Supports:** color (background, link, ~~text~~), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, link, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign ## Content @@ -611,7 +611,7 @@ Display the publish date for an entry such as a post or page. ([Source](https:// - **Name:** core/post-date - **Category:** theme -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** displayType, format, isLink, textAlign ## Excerpt @@ -620,7 +620,7 @@ Display the excerpt. ([Source](https://github.com/WordPress/gutenberg/tree/trunk - **Name:** core/post-excerpt - **Category:** theme -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** excerptLength, moreText, showMoreOnNewLine, textAlign ## Featured Image @@ -629,7 +629,7 @@ Display a post's featured image. ([Source](https://github.com/WordPress/gutenber - **Name:** core/post-featured-image - **Category:** theme -- **Supports:** align (center, full, left, right, wide), color (~~background~~, ~~text~~), spacing (margin, padding), ~~html~~ +- **Supports:** align (center, full, left, right, wide), color (~~background~~, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), ~~html~~ - **Attributes:** aspectRatio, customGradient, customOverlayColor, dimRatio, gradient, height, isLink, linkTarget, overlayColor, rel, scale, sizeSlug, useFirstImageFromPost, width ## Post Navigation Link @@ -638,7 +638,7 @@ Displays the next or previous post link that is adjacent to the current post. ([ - **Name:** core/post-navigation-link - **Category:** theme -- **Supports:** color (background, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** color (background, link, text), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** arrow, label, linkLabel, showTitle, taxonomy, textAlign, type ## Post Template @@ -648,7 +648,7 @@ Contains the block elements used to render a post, like the title, date, feature - **Name:** core/post-template - **Category:** theme - **Parent:** core/query -- **Supports:** align (full, wide), color (background, gradients, link, text), layout, spacing (blockGap), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), layout, spacing (blockGap), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** ## Post Terms @@ -657,7 +657,7 @@ Post terms. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages - **Name:** core/post-terms - **Category:** theme -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** prefix, separator, suffix, term, textAlign ## Time To Read @@ -667,7 +667,7 @@ Show minutes required to finish reading the post. ([Source](https://github.com/W - **Name:** core/post-time-to-read - **Experimental:** true - **Category:** theme -- **Supports:** color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign ## Title @@ -676,7 +676,7 @@ Displays the title of a post, page, or any other content-type. ([Source](https:/ - **Name:** core/post-title - **Category:** theme -- **Supports:** align (full, wide), color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** isLink, level, linkTarget, rel, textAlign ## Preformatted @@ -685,7 +685,7 @@ Add text that respects your spacing and tabs, and also allows styling. ([Source] - **Name:** core/preformatted - **Category:** text -- **Supports:** anchor, color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** anchor, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** content ## Pullquote @@ -694,7 +694,7 @@ Give special visual emphasis to a quote from your text. ([Source](https://github - **Name:** core/pullquote - **Category:** text -- **Supports:** align (full, left, right, wide), anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** align (full, left, right, wide), anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** citation, textAlign, value ## Query Loop @@ -713,7 +713,7 @@ Contains the block elements used to render content when no query results are fou - **Name:** core/query-no-results - **Category:** theme - **Parent:** core/query -- **Supports:** align, color (background, gradients, link, text), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** align, color (background, gradients, link, text), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** ## Pagination @@ -722,8 +722,7 @@ Displays a paginated navigation to next/previous set of posts, when applicable. - **Name:** core/query-pagination - **Category:** theme -- **Parent:** core/query -- **Supports:** align, color (background, gradients, link, text), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** align, color (background, gradients, link, text), interactivity (clientNavigation), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** paginationArrow, showLabel ## Next Page @@ -733,7 +732,7 @@ Displays the next posts page link. ([Source](https://github.com/WordPress/gutenb - **Name:** core/query-pagination-next - **Category:** theme - **Parent:** core/query-pagination -- **Supports:** color (background, gradients, ~~text~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** color (background, gradients, ~~text~~), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** label ## Page Numbers @@ -743,7 +742,7 @@ Displays a list of page numbers for pagination. ([Source](https://github.com/Wor - **Name:** core/query-pagination-numbers - **Category:** theme - **Parent:** core/query-pagination -- **Supports:** color (background, gradients, ~~text~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** color (background, gradients, ~~text~~), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** midSize ## Previous Page @@ -753,7 +752,7 @@ Displays the previous posts page link. ([Source](https://github.com/WordPress/gu - **Name:** core/query-pagination-previous - **Category:** theme - **Parent:** core/query-pagination -- **Supports:** color (background, gradients, ~~text~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** color (background, gradients, ~~text~~), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** label ## Query Title @@ -762,7 +761,7 @@ Display the query title. ([Source](https://github.com/WordPress/gutenberg/tree/t - **Name:** core/query-title - **Category:** theme -- **Supports:** align (full, wide), color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** level, showPrefix, showSearchTerm, textAlign, type ## Quote @@ -771,7 +770,7 @@ Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Ju - **Name:** core/quote - **Category:** text -- **Supports:** anchor, color (background, gradients, heading, link, text), layout (~~allowEditing~~), spacing (blockGap), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** anchor, color (background, gradients, heading, link, text), interactivity (clientNavigation), layout (~~allowEditing~~), spacing (blockGap), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** align, citation, value ## Read More @@ -780,7 +779,7 @@ Displays the link of a post, page, or any other content-type. ([Source](https:// - **Name:** core/read-more - **Category:** theme -- **Supports:** color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** content, linkTarget ## RSS @@ -789,7 +788,7 @@ Display entries from any RSS or Atom feed. ([Source](https://github.com/WordPres - **Name:** core/rss - **Category:** widgets -- **Supports:** align, ~~html~~ +- **Supports:** align, interactivity (clientNavigation), ~~html~~ - **Attributes:** blockLayout, columns, displayAuthor, displayDate, displayExcerpt, excerptLength, feedURL, itemsToShow ## Search @@ -807,7 +806,7 @@ Create a break between ideas or sections with a horizontal separator. ([Source]( - **Name:** core/separator - **Category:** design -- **Supports:** align (center, full, wide), anchor, color (background, gradients, ~~enableContrastChecker~~, ~~text~~), spacing (margin) +- **Supports:** align (center, full, wide), anchor, color (background, gradients, ~~enableContrastChecker~~, ~~text~~), interactivity (clientNavigation), spacing (margin) - **Attributes:** opacity ## Shortcode @@ -825,7 +824,7 @@ Display an image to represent this site. Update this block and the changes apply - **Name:** core/site-logo - **Category:** theme -- **Supports:** align, color (~~background~~, ~~text~~), spacing (margin, padding), ~~alignWide~~, ~~html~~ +- **Supports:** align, color (~~background~~, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), ~~alignWide~~, ~~html~~ - **Attributes:** isLink, linkTarget, shouldSyncIcon, width ## Site Tagline @@ -834,7 +833,7 @@ Describe in a few words what the site is about. The tagline can be used in searc - **Name:** core/site-tagline - **Category:** theme -- **Supports:** align (full, wide), color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign ## Site Title @@ -843,7 +842,7 @@ Displays the name of this site. Update the block, and the changes apply everywhe - **Name:** core/site-title - **Category:** theme -- **Supports:** align (full, wide), color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** isLink, level, linkTarget, textAlign ## Social Icon @@ -853,7 +852,7 @@ Display an icon linking to a social media profile or site. ([Source](https://git - **Name:** core/social-link - **Category:** widgets - **Parent:** core/social-links -- **Supports:** ~~html~~, ~~reusable~~ +- **Supports:** interactivity (clientNavigation), ~~html~~, ~~reusable~~ - **Attributes:** label, rel, service, url ## Social Icons @@ -862,7 +861,7 @@ Display icons linking to your social media profiles or sites. ([Source](https:// - **Name:** core/social-links - **Category:** widgets -- **Supports:** align (center, left, right), anchor, color (background, gradients, ~~enableContrastChecker~~, ~~text~~), layout (default, ~~allowInheriting~~, ~~allowSwitching~~, ~~allowVerticalAlignment~~), spacing (blockGap, margin, padding, units) +- **Supports:** align (center, left, right), anchor, color (background, gradients, ~~enableContrastChecker~~, ~~text~~), interactivity (clientNavigation), layout (default, ~~allowInheriting~~, ~~allowSwitching~~, ~~allowVerticalAlignment~~), spacing (blockGap, margin, padding, units) - **Attributes:** customIconBackgroundColor, customIconColor, iconBackgroundColor, iconBackgroundColorValue, iconColor, iconColorValue, openInNewTab, showLabels, size ## Spacer @@ -871,7 +870,7 @@ Add white space between blocks and customize its height. ([Source](https://githu - **Name:** core/spacer - **Category:** design -- **Supports:** anchor, spacing (margin) +- **Supports:** anchor, interactivity (clientNavigation), spacing (margin) - **Attributes:** height, width ## Table @@ -880,7 +879,7 @@ Create structured content in rows and columns to display information. ([Source]( - **Name:** core/table - **Category:** text -- **Supports:** align, anchor, color (background, gradients, text), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** align, anchor, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** body, caption, foot, hasFixedLayout, head ## Table of Contents @@ -890,7 +889,7 @@ Summarize your post with a list of headings. Add HTML anchors to Heading blocks - **Name:** core/table-of-contents - **Experimental:** true - **Category:** layout -- **Supports:** color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** headings, onlyIncludeCurrentPage ## Tag Cloud @@ -899,7 +898,7 @@ A cloud of your most used tags. ([Source](https://github.com/WordPress/gutenberg - **Name:** core/tag-cloud - **Category:** widgets -- **Supports:** align, spacing (margin, padding), typography (lineHeight), ~~html~~ +- **Supports:** align, interactivity (clientNavigation), spacing (margin, padding), typography (lineHeight), ~~html~~ - **Attributes:** largestFontSize, numberOfTags, showTagCounts, smallestFontSize, taxonomy ## Template Part @@ -908,7 +907,7 @@ Edit the different global regions of your site, like the header, footer, sidebar - **Name:** core/template-part - **Category:** theme -- **Supports:** align, ~~html~~, ~~renaming~~, ~~reusable~~ +- **Supports:** align, interactivity (clientNavigation), ~~html~~, ~~renaming~~, ~~reusable~~ - **Attributes:** area, slug, tagName, theme ## Term Description @@ -917,7 +916,7 @@ Display the description of categories, tags and custom taxonomies when viewing a - **Name:** core/term-description - **Category:** theme -- **Supports:** align (full, wide), color (background, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (full, wide), color (background, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign ## Text Columns (deprecated) @@ -926,7 +925,7 @@ This block is deprecated. Please use the Columns block instead. ([Source](https: - **Name:** core/text-columns - **Category:** design -- **Supports:** ~~inserter~~ +- **Supports:** interactivity (clientNavigation), ~~inserter~~ - **Attributes:** columns, content, width ## Verse @@ -935,7 +934,7 @@ Insert poetry. Use special spacing formats. Or quote song lyrics. ([Source](http - **Name:** core/verse - **Category:** text -- **Supports:** anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight) +- **Supports:** anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight) - **Attributes:** content, textAlign ## Video @@ -944,7 +943,7 @@ Embed a video from your media library or upload a new one. ([Source](https://git - **Name:** core/video - **Category:** media -- **Supports:** align, anchor, spacing (margin, padding) +- **Supports:** align, anchor, interactivity (clientNavigation), spacing (margin, padding) - **Attributes:** autoplay, caption, controls, id, loop, muted, playsInline, poster, preload, src, tracks diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 6287a822238a23..0d2520ff9682a4 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -695,7 +695,7 @@ public function __construct( $theme_json = array(), $origin = 'theme' ) { $origin = 'theme'; } - $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json ); $registry = WP_Block_Type_Registry::get_instance(); $valid_block_names = array_keys( $registry->get_all_registered() ); $valid_element_names = array_keys( static::ELEMENTS ); @@ -2638,7 +2638,7 @@ public function get_root_layout_rules( $selector, $block_metadata ) { $css .= '--wp--style--global--wide-size: ' . $wide_size . ';'; } - $css .= '}'; + $css .= ' }'; if ( $use_root_padding ) { // Top and bottom padding are applied to the outer block container. @@ -2723,7 +2723,7 @@ protected static function get_metadata_boolean( $data, $path, $default_value = f * @since 5.8.0 * @since 5.9.0 Duotone preset also has origins. * - * @param WP_Theme_JSON $incoming Data to merge. + * @param WP_Theme_JSON_Gutenberg $incoming Data to merge. */ public function merge( $incoming ) { $incoming_data = $incoming->get_raw_data(); @@ -2997,7 +2997,7 @@ protected static function filter_slugs( $node, $slugs ) { public static function remove_insecure_properties( $theme_json ) { $sanitized = array(); - $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); + $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json ); $valid_block_names = array_keys( static::get_blocks_metadata() ); $valid_element_names = array_keys( static::ELEMENTS ); diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index 97d8e4534d899c..ec0d2c6bb0181e 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -1,8 +1,8 @@ get_data(); @@ -227,7 +228,7 @@ protected static function has_same_registered_blocks( $origin ) { * * @type bool $with_supports Whether to include theme supports in the data. Default true. * } - * @return WP_Theme_JSON Entity that holds theme data. + * @return WP_Theme_JSON_Gutenberg Entity that holds theme data. */ public static function get_theme_data( $deprecated = array(), $options = array() ) { if ( ! empty( $deprecated ) ) { @@ -251,7 +252,7 @@ public static function get_theme_data( $deprecated = array(), $options = array() * * @since 6.1.0 * - * @param WP_Theme_JSON_Data Class to access and update the underlying data. + * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. */ $theme_json = apply_filters( 'wp_theme_json_data_theme', new WP_Theme_JSON_Data_Gutenberg( $theme_json_data, 'theme' ) ); $theme_json_data = $theme_json->get_data(); @@ -358,7 +359,7 @@ public static function get_theme_data( $deprecated = array(), $options = array() * * @since 6.1.0 * - * @return WP_Theme_JSON + * @return WP_Theme_JSON_Gutenberg */ public static function get_block_data() { $registry = WP_Block_Type_Registry::get_instance(); @@ -389,7 +390,7 @@ public static function get_block_data() { * * @since 6.1.0 * - * @param WP_Theme_JSON_Data Class to access and update the underlying data. + * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. */ $theme_json = apply_filters( 'wp_theme_json_data_blocks', new WP_Theme_JSON_Data_Gutenberg( $config, 'blocks' ) ); $config = $theme_json->get_data(); @@ -502,7 +503,7 @@ public static function get_user_data_from_wp_global_styles( $theme, $create_post * * @since 5.9.0 * - * @return WP_Theme_JSON Entity that holds styles for user data. + * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data. */ public static function get_user_data() { if ( null !== static::$user && static::has_same_registered_blocks( 'user' ) ) { @@ -523,7 +524,7 @@ public static function get_user_data() { * * @since 6.1.0 * - * @param WP_Theme_JSON_Data Class to access and update the underlying data. + * @param WP_Theme_JSON_Data_Gutenberg Class to access and update the underlying data. */ $theme_json = apply_filters( 'wp_theme_json_data_user', new WP_Theme_JSON_Data_Gutenberg( $config, 'custom' ) ); $config = $theme_json->get_data(); @@ -583,7 +584,7 @@ public static function get_user_data() { * @param string $origin Optional. To what level should we merge data:'default', 'blocks', 'theme' or 'custom'. * 'custom' is used as default value as well as fallback value if the origin is unknown. * - * @return WP_Theme_JSON + * @return WP_Theme_JSON_Gutenberg */ public static function get_merged_data( $origin = 'custom' ) { if ( is_array( $origin ) ) { diff --git a/lib/class-wp-theme-json-schema-gutenberg.php b/lib/class-wp-theme-json-schema-gutenberg.php new file mode 100644 index 00000000000000..8373e133c5aec8 --- /dev/null +++ b/lib/class-wp-theme-json-schema-gutenberg.php @@ -0,0 +1,154 @@ + 'border.radius', + 'spacing.customMargin' => 'spacing.margin', + 'spacing.customPadding' => 'spacing.padding', + 'typography.customLineHeight' => 'typography.lineHeight', + ); + + /** + * Function that migrates a given theme.json structure to the last version. + * + * @since 5.9.0 + * + * @param array $theme_json The structure to migrate. + * + * @return array The structure in the last version. + */ + public static function migrate( $theme_json ) { + if ( ! isset( $theme_json['version'] ) ) { + $theme_json = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + ); + } + + if ( 1 === $theme_json['version'] ) { + $theme_json = self::migrate_v1_to_v2( $theme_json ); + } + + return $theme_json; + } + + /** + * Removes the custom prefixes for a few properties + * that were part of v1: + * + * 'border.customRadius' => 'border.radius', + * 'spacing.customMargin' => 'spacing.margin', + * 'spacing.customPadding' => 'spacing.padding', + * 'typography.customLineHeight' => 'typography.lineHeight', + * + * @since 5.9.0 + * + * @param array $old Data to migrate. + * + * @return array Data without the custom prefixes. + */ + private static function migrate_v1_to_v2( $old ) { + // Copy everything. + $new = $old; + + // Overwrite the things that changed. + if ( isset( $old['settings'] ) ) { + $new['settings'] = self::rename_paths( $old['settings'], self::V1_TO_V2_RENAMED_PATHS ); + } + + // Set the new version. + $new['version'] = 2; + + return $new; + } + + /** + * Processes the settings subtree. + * + * @since 5.9.0 + * + * @param array $settings Array to process. + * @param array $paths_to_rename Paths to rename. + * + * @return array The settings in the new format. + */ + private static function rename_paths( $settings, $paths_to_rename ) { + $new_settings = $settings; + + // Process any renamed/moved paths within default settings. + self::rename_settings( $new_settings, $paths_to_rename ); + + // Process individual block settings. + if ( isset( $new_settings['blocks'] ) && is_array( $new_settings['blocks'] ) ) { + foreach ( $new_settings['blocks'] as &$block_settings ) { + self::rename_settings( $block_settings, $paths_to_rename ); + } + } + + return $new_settings; + } + + /** + * Processes a settings array, renaming or moving properties. + * + * @since 5.9.0 + * + * @param array $settings Reference to settings either defaults or an individual block's. + * @param array $paths_to_rename Paths to rename. + */ + private static function rename_settings( &$settings, $paths_to_rename ) { + foreach ( $paths_to_rename as $original => $renamed ) { + $original_path = explode( '.', $original ); + $renamed_path = explode( '.', $renamed ); + $current_value = _wp_array_get( $settings, $original_path, null ); + + if ( null !== $current_value ) { + _wp_array_set( $settings, $renamed_path, $current_value ); + self::unset_setting_by_path( $settings, $original_path ); + } + } + } + + /** + * Removes a property from within the provided settings by its path. + * + * @since 5.9.0 + * + * @param array $settings Reference to the current settings array. + * @param array $path Path to the property to be removed. + */ + private static function unset_setting_by_path( &$settings, $path ) { + $tmp_settings = &$settings; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $last_key = array_pop( $path ); + foreach ( $path as $key ) { + $tmp_settings = &$tmp_settings[ $key ]; + } + + unset( $tmp_settings[ $last_key ] ); + } +} diff --git a/lib/compat/wordpress-6.5/class-wp-script-modules.php b/lib/compat/wordpress-6.5/class-wp-script-modules.php index f6a2a348f92ef3..d7eaff177823cc 100644 --- a/lib/compat/wordpress-6.5/class-wp-script-modules.php +++ b/lib/compat/wordpress-6.5/class-wp-script-modules.php @@ -212,10 +212,37 @@ public function print_script_module_preloads() { * Prints the import map using a script tag with a type="importmap" attribute. * * @since 6.5.0 + * + * @global WP_Scripts $wp_scripts The WP_Scripts object for printing the polyfill. */ public function print_import_map() { $import_map = $this->get_import_map(); if ( ! empty( $import_map['imports'] ) ) { + global $wp_scripts; + if ( isset( $wp_scripts ) ) { + /* + * In Core, the polyfill is registered with a different approach. + * See: https://github.com/WordPress/wordpress-develop/blob/4b23ba81ddb067110e41d05550de7f2a4f09dad3/src/wp-includes/script-loader.php#L99 + */ + wp_register_script( + 'wp-polyfill-importmap', + gutenberg_url( '/build/modules/importmap-polyfill.min.js' ), + array(), + '1.8.2', + true + ); + wp_print_inline_script_tag( + wp_get_script_polyfill( + $wp_scripts, + array( + 'HTMLScriptElement.supports && HTMLScriptElement.supports("importmap")' => 'wp-polyfill-importmap', + ) + ), + array( + 'id' => 'wp-load-polyfill-importmap', + ) + ); + } wp_print_inline_script_tag( wp_json_encode( $import_map, JSON_HEX_TAG | JSON_HEX_AMP ), array( diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php new file mode 100644 index 00000000000000..154621ead50fbe --- /dev/null +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php @@ -0,0 +1,221 @@ +slug = sanitize_title( $slug ); + + // Data or json are lazy loaded and validated in get_data(). + if ( is_array( $data_or_file ) ) { + $this->data = $data_or_file; + } else { + $this->src = $data_or_file; + } + + if ( $this->slug !== $slug ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Font collection slug. */ + sprintf( __( 'Font collection slug "%s" is not valid. Slugs must use only alphanumeric characters, dashes, and underscores.', 'gutenberg' ), $slug ), + '6.5.0' + ); + } + } + + /** + * Retrieves the font collection data. + * + * @since 6.5.0 + * + * @return array|WP_Error An array containing the font collection data, or a WP_Error on failure. + */ + public function get_data() { + // If we have a JSON config, load it and cache the data if it's valid. + if ( $this->src && empty( $this->data ) ) { + $data = $this->load_from_json( $this->src ); + if ( is_wp_error( $data ) ) { + return $data; + } + + $this->data = $data; + } + + // Validate required properties are not empty. + $data = $this->validate_data( $this->data ); + if ( is_wp_error( $data ) ) { + return $data; + } + + // Set defaults for optional properties. + return wp_parse_args( + $data, + array( + 'description' => '', + 'categories' => array(), + ) + ); + } + + /** + * Loads font collection data from a JSON file or URL. + * + * @since 6.5.0 + * + * @param string $file_or_url File path or URL to a JSON file containing the font collection data. + * @return array|WP_Error An array containing the font collection data on success, + * else an instance of WP_Error on failure. + */ + private function load_from_json( $file_or_url ) { + $url = wp_http_validate_url( $file_or_url ); + $file = file_exists( $file_or_url ) ? wp_normalize_path( realpath( $file_or_url ) ) : false; + + if ( ! $url && ! $file ) { + // translators: %s: File path or URL to font collection JSON file. + $message = __( 'Font collection JSON file is invalid or does not exist.', 'gutenberg' ); + _doing_it_wrong( __METHOD__, $message, '6.5.0' ); + return new WP_Error( 'font_collection_json_missing', $message ); + } + + return $url ? $this->load_from_url( $url ) : $this->load_from_file( $file ); + } + + /** + * Loads the font collection data from a JSON file path. + * + * @since 6.5.0 + * + * @param string $file File path to a JSON file containing the font collection data. + * @return array|WP_Error An array containing the font collection data on success, + * else an instance of WP_Error on failure. + */ + private function load_from_file( $file ) { + $data = wp_json_file_decode( $file, array( 'associative' => true ) ); + if ( empty( $data ) ) { + return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection JSON file contents.', 'gutenberg' ) ); + } + + return $data; + } + + /** + * Loads the font collection data from a JSON file URL. + * + * @since 6.5.0 + * + * @param string $url URL to a JSON file containing the font collection data. + * @return array|WP_Error An array containing the font collection data on success, + * else an instance of WP_Error on failure. + */ + private function load_from_url( $url ) { + // Limit key to 167 characters to avoid failure in the case of a long URL. + $transient_key = substr( 'wp_font_collection_url_' . $url, 0, 167 ); + $data = get_site_transient( $transient_key ); + + if ( false === $data ) { + $response = wp_safe_remote_get( $url ); + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + // translators: %s: Font collection URL. + return new WP_Error( 'font_collection_request_error', sprintf( __( 'Error fetching the font collection data from "%s".', 'gutenberg' ), $url ) ); + } + + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( empty( $data ) ) { + return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection data from the REST response JSON.', 'gutenberg' ) ); + } + + // Make sure the data is valid before caching it. + $data = $this->validate_data( $data ); + if ( is_wp_error( $data ) ) { + return $data; + } + + set_site_transient( $transient_key, $data, DAY_IN_SECONDS ); + } + + return $data; + } + + /** + * Validates the font collection configuration. + * + * @since 6.5.0 + * + * @param array $data Font collection configuration to validate. + * @return array|WP_Error Array of data if valid, otherwise a WP_Error instance. + */ + private function validate_data( $data ) { + $required_properties = array( 'name', 'font_families' ); + foreach ( $required_properties as $property ) { + if ( empty( $data[ $property ] ) ) { + $message = sprintf( + // translators: 1: Font collection slug, 2: Missing property name. + __( 'Font collection "%1$s" has missing or empty property: "%2$s."', 'gutenberg' ), + $this->slug, + $property + ); + _doing_it_wrong( __METHOD__, $message, '6.5.0' ); + return new WP_Error( 'font_collection_missing_property', $message ); + } + } + + return $data; + } + } +} diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php similarity index 79% rename from lib/experimental/fonts/font-library/class-wp-font-library.php rename to lib/compat/wordpress-6.5/fonts/class-wp-font-library.php index bc119d35702340..d5db42c499814c 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-library.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-library.php @@ -55,13 +55,14 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio * * @since 6.5.0 * - * @param string $slug Font collection slug. - * @param array $args Font collection config options. - * See {@see wp_register_font_collection()} for the supported fields. + * @param string $slug Font collection slug. + * @param array $data_or_file Font collection data array or a file path or url to a JSON file + * containing the font collection. + * See {@see wp_register_font_collection()} for the supported fields. * @return WP_Font_Collection|WP_Error A font collection if registration was successful, else WP_Error. */ - public static function register_font_collection( $slug, $args = array() ) { - $new_collection = new WP_Font_Collection( $slug, $args ); + public static function register_font_collection( $slug, $data_or_file ) { + $new_collection = new WP_Font_Collection( $slug, $data_or_file ); if ( self::is_collection_registered( $new_collection->slug ) ) { $error_message = sprintf( @@ -80,29 +81,12 @@ public static function register_font_collection( $slug, $args = array() ) { return $new_collection; } - /** - * Register a new font collection from a json file. - * - * @since 6.5.0 - * - * @param string $file_or_url File path or URL to a JSON file containing the font collection data. - * @return WP_Font_Collection|WP_Error A font collection if registration was successful, else WP_Error. - */ - public static function register_font_collection_from_json( $file_or_url ) { - $args = WP_Font_Collection::load_from_json( $file_or_url ); - if ( is_wp_error( $args ) ) { - return $args; - } - - return self::register_font_collection( $args['slug'], $args ); - } - /** * Unregisters a previously registered font collection. * * @since 6.5.0 * - * @param string $collection_slug Font collection slug. + * @param string $slug Font collection slug. * @return bool True if the font collection was unregistered successfully and false otherwise. */ public static function unregister_font_collection( $slug ) { @@ -110,7 +94,7 @@ public static function unregister_font_collection( $slug ) { _doing_it_wrong( __METHOD__, /* translators: %s: Font collection slug. */ - sprintf( __( 'Font collection "%s" not found.', 'default' ), $slug ), + sprintf( __( 'Font collection "%s" not found.' ), $slug ), '6.5.0' ); return false; @@ -148,7 +132,8 @@ public static function get_font_collections() { * @since 6.5.0 * * @param string $slug Font collection slug. - * @return WP_Font_Collection Font collection object. + * @return WP_Font_Collection|WP_Error Font collection object, + * or WP_Error object if the font collection doesn't exist. */ public static function get_font_collection( $slug ) { if ( array_key_exists( $slug, self::$collections ) ) { @@ -157,8 +142,6 @@ public static function get_font_collection( $slug ) { return new WP_Error( 'font_collection_not_found', 'Font collection not found.' ); } - - /** * Sets the allowed mime types for fonts. * diff --git a/lib/experimental/fonts/font-library/class-wp-font-utils.php b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php similarity index 56% rename from lib/experimental/fonts/font-library/class-wp-font-utils.php rename to lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php index e6fdc5ae08f5d8..792a5aaa80eef6 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-utils.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php @@ -1,8 +1,8 @@ '', - 'fontStyle' => 'normal', - 'fontWeight' => '400', - 'fontStretch' => '100%', - 'unicodeRange' => 'U+0-10FFFF', - ) + $defaults = array( + 'fontFamily' => '', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'fontStretch' => '100%', + 'unicodeRange' => 'U+0-10FFFF', ); + $settings = wp_parse_args( $settings, $defaults ); - // Convert all values to lowercase for comparison. - // Font family names may use multibyte characters. $font_family = mb_strtolower( $settings['fontFamily'] ); $font_style = strtolower( $settings['fontStyle'] ); $font_weight = strtolower( $settings['fontWeight'] ); @@ -98,8 +96,7 @@ public static function get_font_face_slug( $settings ) { $unicode_range = strtoupper( $settings['unicodeRange'] ); // Convert weight keywords to numeric strings. - $font_weight = str_replace( 'normal', '400', $font_weight ); - $font_weight = str_replace( 'bold', '700', $font_weight ); + $font_weight = str_replace( array( 'normal', 'bold' ), array( '400', '700' ), $font_weight ); // Convert stretch keywords to numeric strings. $font_stretch_map = array( @@ -133,5 +130,80 @@ function ( $elem ) { return join( ';', $slug_elements ); } + + /** + * Sanitize a tree of data using a schema that defines the sanitization to apply to each key. + * + * It removes the keys not in the schema and applies the sanitizer to the values. + * + * @since 6.5.0 + * + * @access private + * + * @param array $tree The data to sanitize. + * @param array $schema The schema used for sanitization. + * + * @return array The sanitized data. + */ + public static function sanitize_from_schema( $tree, $schema ) { + if ( ! is_array( $tree ) || ! is_array( $schema ) ) { + return array(); + } + + foreach ( $tree as $key => $value ) { + // Remove keys not in the schema or with null/empty values. + if ( ! array_key_exists( $key, $schema ) ) { + unset( $tree[ $key ] ); + continue; + } + + $is_value_array = is_array( $value ); + $is_schema_array = is_array( $schema[ $key ] ); + + if ( $is_value_array && $is_schema_array ) { + if ( wp_is_numeric_array( $value ) ) { + // If indexed, process each item in the array. + foreach ( $value as $item_key => $item_value ) { + $tree[ $key ][ $item_key ] = isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) + ? self::sanitize_from_schema( $item_value, $schema[ $key ][0] ) + : self::apply_sanitizer( $item_value, $schema[ $key ][0] ); + } + } else { + // If it is an associative or indexed array., process as a single object. + $tree[ $key ] = self::sanitize_from_schema( $value, $schema[ $key ] ); + } + } elseif ( ! $is_value_array && $is_schema_array ) { + // If the value is not an array but the schema is, remove the key. + unset( $tree[ $key ] ); + } elseif ( ! $is_schema_array ) { + // If the schema is not an array, apply the sanitizer to the value. + $tree[ $key ] = self::apply_sanitizer( $value, $schema[ $key ] ); + } + + // Remove keys with null/empty values. + if ( empty( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); + } + } + + return $tree; + } + + /** + * Apply the sanitizer to the value. + * + * @since 6.5.0 + * @param mixed $value The value to sanitize. + * @param mixed $sanitizer The sanitizer to apply. + * + * @return mixed The sanitized value. + */ + private static function apply_sanitizer( $value, $sanitizer ) { + if ( null === $sanitizer ) { + return $value; + + } + return call_user_func( $sanitizer, $value ); + } } } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php similarity index 86% rename from lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php rename to lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php index 6772a8c3b44088..372f798f0d2bf9 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php @@ -84,7 +84,7 @@ public function get_items( $request ) { if ( $page > $max_pages && $total_items > 0 ) { return new WP_Error( 'rest_post_invalid_page_number', - __( 'The page number requested is larger than the number of pages available.', 'default' ), + __( 'The page number requested is larger than the number of pages available.' ), array( 'status' => 400 ) ); } @@ -94,8 +94,10 @@ public function get_items( $request ) { $items = array(); foreach ( $collections_page as $collection ) { $item = $this->prepare_item_for_response( $collection, $request ); + + // If there's an error loading a collection, skip it and continue loading valid collections. if ( is_wp_error( $item ) ) { - return $item; + continue; } $item = $this->prepare_response_for_collection( $item ); $items[] = $item; @@ -148,32 +150,39 @@ public function get_item( $request ) { return $collection; } - $item = $this->prepare_item_for_response( $collection, $request ); - - if ( is_wp_error( $item ) ) { - return $item; - } - - return $item; + return $this->prepare_item_for_response( $collection, $request ); } - /* + /** * Prepare a single collection output for response. * * @since 6.5.0 * * @param WP_Font_Collection $collection Collection object. * @param WP_REST_Request $request Request object. - * @return array|WP_Error + * @return WP_REST_Response Response object. */ public function prepare_item_for_response( $collection, $request ) { $fields = $this->get_fields_for_response( $request ); $item = array(); - $config_fields = array( 'slug', 'name', 'description', 'font_families', 'categories' ); - foreach ( $config_fields as $field ) { - if ( in_array( $field, $fields, true ) ) { - $item[ $field ] = $collection->$field; + if ( rest_is_field_included( 'slug', $fields ) ) { + $item['slug'] = $collection->slug; + } + + // If any data fields are requested, get the collection data. + $data_fields = array( 'name', 'description', 'font_families', 'categories' ); + if ( ! empty( array_intersect( $fields, $data_fields ) ) ) { + $collection_data = $collection->get_data(); + if ( is_wp_error( $collection_data ) ) { + $collection_data->add_data( array( 'status' => 500 ) ); + return $collection_data; + } + + foreach ( $data_fields as $field ) { + if ( rest_is_field_included( $field, $fields ) ) { + $item[ $field ] = $collection_data[ $field ]; + } } } @@ -195,9 +204,9 @@ public function prepare_item_for_response( $collection, $request ) { * * @since 6.5.0 * - * @param WP_REST_Response $response The response object. + * @param WP_REST_Response $response The response object. * @param WP_Font_Collection $collection The Font Collection object. - * @param WP_REST_Request $request Request used to generate the response. + * @param WP_REST_Request $request Request used to generate the response. */ return apply_filters( 'rest_prepare_font_collection', $response, $collection, $request ); } @@ -262,7 +271,7 @@ public function get_item_schema() { * @return array Links for the given font collection. */ protected function prepare_links( $collection ) { - $links = array( + return array( 'self' => array( 'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $collection->slug ) ), ), @@ -270,7 +279,6 @@ protected function prepare_links( $collection ) { 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), ), ); - return $links; } /** @@ -305,17 +313,17 @@ public function get_collection_params() { * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. */ public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( - 'rest_cannot_read', - __( 'Sorry, you are not allowed to use the Font Library on this site.', 'gutenberg' ), - array( - 'status' => rest_authorization_required_code(), - ) - ); + if ( current_user_can( 'edit_theme_options' ) ) { + return true; } - return true; + + return new WP_Error( + 'rest_cannot_read', + __( 'Sorry, you are not allowed to use the Font Library on this site.', 'gutenberg' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); } } } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php similarity index 99% rename from lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php rename to lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php index 01ffce1eea4838..efecd6c6821c32 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php @@ -151,7 +151,7 @@ public function get_item_permissions_check( $request ) { * * @param string $value Encoded JSON string of font face settings. * @param WP_REST_Request $request Request object. - * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object. + * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object. */ public function validate_create_font_face_settings( $value, $request ) { $settings = json_decode( $value, true ); @@ -698,7 +698,7 @@ protected function get_parent_font_family_post( $font_family_id ) { */ protected function prepare_links( $post ) { // Entity meta. - $links = array( + return array( 'self' => array( 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ), ), @@ -709,8 +709,6 @@ protected function prepare_links( $post ) { 'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ), ), ); - - return $links; } /** @@ -788,7 +786,7 @@ public function handle_font_file_upload_error( $file, $message ) { $status = 500; $code = 'rest_font_upload_unknown_error'; - if ( __( 'Sorry, you are not allowed to upload this file type.', 'default' ) === $message ) { + if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $message ) { $status = 400; $code = 'rest_font_upload_invalid_file_type'; } @@ -799,7 +797,7 @@ public function handle_font_file_upload_error( $file, $message ) { /** * Returns relative path to an uploaded font file. * - * The path is relative to the current fonts dir. + * The path is relative to the current fonts directory. * * @since 6.5.0 * @access private diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php similarity index 99% rename from lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php rename to lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php index 8d178a2848a35e..7586fe0209329c 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php @@ -77,7 +77,7 @@ public function get_item_permissions_check( $request ) { * * @param string $value Encoded JSON string of font family settings. * @param WP_REST_Request $request Request object. - * @return false|WP_Error True if the settings are valid, otherwise a WP_Error object. + * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object. */ public function validate_font_family_settings( $value, $request ) { $settings = json_decode( $value, true ); @@ -405,7 +405,6 @@ public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CRE * * @param int $font_family_id Font family post ID. * @return int[] Array of child font face post IDs. - * . */ protected function get_font_face_ids( $font_family_id ) { $query = new WP_Query( diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/compat/wordpress-6.5/fonts/fonts.php similarity index 83% rename from lib/experimental/fonts/font-library/font-library.php rename to lib/compat/wordpress-6.5/fonts/fonts.php index bcac6f627fe672..28b019b9c1fa61 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/compat/wordpress-6.5/fonts/fonts.php @@ -19,7 +19,7 @@ * * @since 6.5.0 */ -function gutenberg_init_font_library_routes() { +function gutenberg_create_initial_post_types() { // @core-merge: This code will go into Core's `create_initial_post_types()`. $args = array( 'labels' => array( @@ -82,13 +82,30 @@ function gutenberg_init_font_library_routes() { 'autosave_rest_controller_class' => 'stdClass', ) ); +} +/** + * Initializes REST routes. + * + * @since 6.5 + */ +function gutenberg_create_initial_rest_routes() { // @core-merge: This code will go into Core's `create_initial_rest_routes()`. $font_collections_controller = new WP_REST_Font_Collections_Controller(); $font_collections_controller->register_routes(); } -add_action( 'rest_api_init', 'gutenberg_init_font_library_routes' ); +/** + * Initializes REST routes and post types. + * + * @since 6.5 + */ +function gutenberg_init_font_library() { + gutenberg_create_initial_post_types(); + gutenberg_create_initial_rest_routes(); +} + +add_action( 'rest_api_init', 'gutenberg_init_font_library' ); if ( ! function_exists( 'wp_register_font_collection' ) ) { @@ -97,9 +114,10 @@ function gutenberg_init_font_library_routes() { * * @since 6.5.0 * - * @param string $slug_or_file Font collection slug or path/url to a JSON file defining the font collection. - * @param string[] $args { - * Optional. Font collection associative array of configuration options. + * @param string $slug Font collection slug or path/url to a JSON file defining the font collection. + * @param array|string $data_or_file { + * Font collection associative array of data, or a file path or url to a JSON + * file containing the font collection. * * @type string $name Name of the font collection. * @type string $description Description of the font collection. @@ -107,24 +125,10 @@ function gutenberg_init_font_library_routes() { * @type array $categories Array of categories for the fonts that are in the collection. * } * @return WP_Font_Collection|WP_Error A font collection is it was registered - * successfully, else WP_Error. + * successfully, or WP_Error object on failure. */ - function wp_register_font_collection( $slug, $args = array() ) { - return WP_Font_Library::register_font_collection( $slug, $args ); - } -} - -if ( ! function_exists( 'wp_register_font_collection_from_json' ) ) { - /** - * Registers a new Font Collection from a json file in the Font Library. - * - * @since 6.5.0 - * - * @param string $file_or_url File path or URL to a JSON file containing the font collection data. - * @return WP_Font_Collection|WP_Error A font collection if registration was successful, else WP_Error. - */ - function wp_register_font_collection_from_json( $file_or_url ) { - return WP_Font_Library::register_font_collection_from_json( $file_or_url ); + function wp_register_font_collection( $slug, $data_or_file ) { + return WP_Font_Library::register_font_collection( $slug, $data_or_file ); } } @@ -135,14 +139,18 @@ function wp_register_font_collection_from_json( $file_or_url ) { * @since 6.5.0 * * @param string $collection_id The font collection ID. + * @return bool True if the font collection was unregistered successfully, else false. */ function wp_unregister_font_collection( $collection_id ) { - WP_Font_Library::unregister_font_collection( $collection_id ); + return WP_Font_Library::unregister_font_collection( $collection_id ); } } -// TODO: update to production font collection URL. -wp_register_font_collection_from_json( 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/01aa57731575bd13f9db8d86ab80a2d74e28a1ac/releases/gutenberg-17.6/collections/google-fonts-with-preview.json' ); +function gutenberg_register_font_collections() { + // TODO: update to production font collection URL. + wp_register_font_collection( 'google-fonts', 'https://raw.githubusercontent.com/WordPress/google-fonts-to-wordpress-collection/01aa57731575bd13f9db8d86ab80a2d74e28a1ac/releases/gutenberg-17.6/collections/google-fonts-with-preview.json' ); +} +add_action( 'init', 'gutenberg_register_font_collections' ); // @core-merge: This code should probably go into Core's src/wp-includes/functions.php. if ( ! function_exists( 'wp_get_font_dir' ) ) { @@ -161,7 +169,6 @@ function wp_unregister_font_collection( $collection_id ) { * @type string $baseurl URL path without subdir. * @type string|false $error False or error message. * } - * * @return array $defaults { * Array of information about the upload directory. * @@ -188,7 +195,15 @@ function wp_get_font_dir( $defaults = array() ) { $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; $defaults['error'] = false; - // Filters the fonts directory data. + /** + * Filters the fonts directory data. + * + * This filter allows developers to modify the fonts directory data. + * + * @since 6.5.0 + * + * @param array $defaults The original fonts directory data. + */ return apply_filters( 'font_dir', $defaults ); } } @@ -204,7 +219,6 @@ function wp_get_font_dir( $defaults = array() ) { * * @param int $post_id Post ID. * @param WP_Post $post Post object. - * @return void */ function _wp_after_delete_font_family( $post_id, $post ) { if ( 'wp_font_family' !== $post->post_type ) { @@ -234,7 +248,6 @@ function _wp_after_delete_font_family( $post_id, $post ) { * * @param int $post_id Post ID. * @param WP_Post $post Post object. - * @return void */ function _wp_before_delete_font_face( $post_id, $post ) { if ( 'wp_font_face' !== $post->post_type ) { @@ -284,7 +297,7 @@ function gutenberg_convert_legacy_font_family_format() { continue; } - $font_faces = $font_family_json['fontFace'] ?? array(); + $font_faces = isset( $font_family_json['fontFace'] ) ? $font_family_json['fontFace'] : array(); unset( $font_family_json['fontFace'] ); // Save wp_font_face posts within the family. @@ -299,7 +312,7 @@ function gutenberg_convert_legacy_font_family_format() { $font_face_id = wp_insert_post( wp_slash( $args ) ); - $file_urls = (array) $font_face['src'] ?? array(); + $file_urls = (array) ( isset( $font_face['src'] ) ? $font_face['src'] : array() ); foreach ( $file_urls as $file_url ) { // continue if the file is not local. @@ -315,7 +328,7 @@ function gutenberg_convert_legacy_font_family_format() { // Update the font family post to remove the font face data. $args = array(); $args['ID'] = $font_family->ID; - $args['post_title'] = $font_family_json['name'] ?? ''; + $args['post_title'] = isset( $font_family_json['name'] ) ? $font_family_json['name'] : ''; $args['post_name'] = sanitize_title( $font_family_json['slug'] ); unset( $font_family_json['name'] ); diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php index ad9e5d7c439533..9cbbfb1d6b6540 100644 --- a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php +++ b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php @@ -18,12 +18,13 @@ class WP_Interactivity_API { * @var array */ private static $directive_processors = array( - 'data-wp-interactive' => 'data_wp_interactive_processor', - 'data-wp-context' => 'data_wp_context_processor', - 'data-wp-bind' => 'data_wp_bind_processor', - 'data-wp-class' => 'data_wp_class_processor', - 'data-wp-style' => 'data_wp_style_processor', - 'data-wp-text' => 'data_wp_text_processor', + 'data-wp-interactive' => 'data_wp_interactive_processor', + 'data-wp-router-region' => 'data_wp_router_region_processor', + 'data-wp-context' => 'data_wp_context_processor', + 'data-wp-bind' => 'data_wp_bind_processor', + 'data-wp-class' => 'data_wp_class_processor', + 'data-wp-style' => 'data_wp_style_processor', + 'data-wp-text' => 'data_wp_text_processor', ); /** @@ -49,6 +50,21 @@ class WP_Interactivity_API { */ private $config_data = array(); + /** + * Flag that indicates whether the `data-wp-router-region` directive has + * been found in the HTML and processed. + * + * The value is saved in a private property of the WP_Interactivity_API + * instance instead of using a static variable inside the processor + * function, which would hold the same value for all instances + * independently of whether they have processed any + * `data-wp-router-region` directive or not. + * + * @since 6.5.0 + * @var bool + */ + private $has_processed_router_region = false; + /** * Gets and/or sets the initial state of an Interactivity API store for a * given namespace. @@ -673,6 +689,88 @@ private function data_wp_text_processor( WP_Interactivity_API_Directives_Process } } } + + /** + * Processes the `data-wp-router-region` directive. + * + * It renders in the footer a set of HTML elements to notify users about + * client-side navigations. More concretely, the elements added are 1) a + * top loading bar to visually inform that a navigation is in progress + * and 2) an `aria-live` region for accessible navigation announcements. + * + * @since 6.5.0 + * + * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. + */ + private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p ) { + if ( ! $p->is_tag_closer() && ! $this->has_processed_router_region ) { + $this->has_processed_router_region = true; + + // Initialize the `core/router` store. + $this->state( + 'core/router', + array( + 'navigation' => array( + 'message' => '', + 'hasStarted' => false, + 'hasFinished' => false, + 'texts' => array( + 'loading' => __( 'Loading page, please wait.' ), + 'loaded' => __( 'Page Loaded.' ), + ), + ), + ) + ); + + $callback = static function () { + echo << +.wp-interactivity-router_loading-bar { + position: fixed; + top: 0; + left: 0; + margin: 0; + padding: 0; + width: 100vw; + max-width: 100vw !important; + height: 4px; + background-color: var(--wp--preset--color--primary, #000); + opacity: 0 +} +.wp-interactivity-router_loading-bar.start-animation { + animation: wp-interactivity-router_loading-bar-start-animation 30s cubic-bezier(0.03, 0.5, 0, 1) forwards +} +.wp-interactivity-router_loading-bar.finish-animation { + animation: wp-interactivity-router_loading-bar-finish-animation 300ms ease-in +} + +@keyframes wp-interactivity-router_loading-bar-start-animation { + 0% { transform: scaleX(0); transform-origin: 0% 0%; opacity: 1 } + 100% { transform: scaleX(1); transform-origin: 0% 0%; opacity: 1 } +} +@keyframes wp-interactivity-router_loading-bar-finish-animation { + 0% { opacity: 1 } + 50% { opacity: 1 } + 100% { opacity: 0 } +} + +
+
+HTML; + }; + add_action( 'wp_footer', $callback ); + } + } } } diff --git a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php index cd7ca7fb902870..32900a78f6d691 100644 --- a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php +++ b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php @@ -31,7 +31,11 @@ function wp_interactivity_process_directives_of_interactive_blocks( $parsed_bloc $block_name = $parsed_block['blockName']; $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); - if ( isset( $block_name ) && isset( $block_type->supports['interactivity'] ) && $block_type->supports['interactivity'] ) { + if ( + isset( $block_name ) && + ( ( isset( $block_type->supports['interactivity'] ) && true === $block_type->supports['interactivity'] ) || + ( isset( $block_type->supports['interactivity']['interactive'] ) && true === $block_type->supports['interactivity']['interactive'] ) ) + ) { // Annotates the root interactive block for processing. $root_interactive_block = array( $block_name, md5( serialize( $parsed_block ) ) ); diff --git a/lib/compat/wordpress-6.5/scripts-modules.php b/lib/compat/wordpress-6.5/scripts-modules.php index ba329b255b1965..740a16581ce7f1 100644 --- a/lib/compat/wordpress-6.5/scripts-modules.php +++ b/lib/compat/wordpress-6.5/scripts-modules.php @@ -20,13 +20,14 @@ * @return WP_Script_Modules The main WP_Script_Modules instance. */ function wp_script_modules(): WP_Script_Modules { - static $instance = null; - if ( is_null( $instance ) ) { - $instance = new WP_Script_Modules(); - $instance->add_hooks(); + global $wp_script_modules; + + if ( ! ( $wp_script_modules instanceof WP_Script_Modules ) ) { + $wp_script_modules = new WP_Script_Modules(); } - return $instance; + return $wp_script_modules; } + wp_script_modules()->add_hooks(); } if ( ! function_exists( 'wp_register_script_module' ) ) { diff --git a/lib/experimental/fonts/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php b/lib/experimental/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php rename to lib/experimental/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider-local.php b/lib/experimental/font-face/bc-layer/class-wp-fonts-provider-local.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider-local.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts-provider-local.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider.php b/lib/experimental/font-face/bc-layer/class-wp-fonts-provider.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts-provider.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-resolver.php b/lib/experimental/font-face/bc-layer/class-wp-fonts-resolver.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-resolver.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts-resolver.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-utils.php b/lib/experimental/font-face/bc-layer/class-wp-fonts-utils.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts-utils.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts-utils.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-fonts.php b/lib/experimental/font-face/bc-layer/class-wp-fonts.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-fonts.php rename to lib/experimental/font-face/bc-layer/class-wp-fonts.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-web-fonts.php b/lib/experimental/font-face/bc-layer/class-wp-web-fonts.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-web-fonts.php rename to lib/experimental/font-face/bc-layer/class-wp-web-fonts.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider-local.php b/lib/experimental/font-face/bc-layer/class-wp-webfonts-provider-local.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider-local.php rename to lib/experimental/font-face/bc-layer/class-wp-webfonts-provider-local.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider.php b/lib/experimental/font-face/bc-layer/class-wp-webfonts-provider.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider.php rename to lib/experimental/font-face/bc-layer/class-wp-webfonts-provider.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-utils.php b/lib/experimental/font-face/bc-layer/class-wp-webfonts-utils.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts-utils.php rename to lib/experimental/font-face/bc-layer/class-wp-webfonts-utils.php diff --git a/lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts.php b/lib/experimental/font-face/bc-layer/class-wp-webfonts.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/class-wp-webfonts.php rename to lib/experimental/font-face/bc-layer/class-wp-webfonts.php diff --git a/lib/experimental/fonts/font-face/bc-layer/webfonts-deprecations.php b/lib/experimental/font-face/bc-layer/webfonts-deprecations.php similarity index 100% rename from lib/experimental/fonts/font-face/bc-layer/webfonts-deprecations.php rename to lib/experimental/font-face/bc-layer/webfonts-deprecations.php diff --git a/lib/experimental/fonts/font-library/class-wp-font-collection.php b/lib/experimental/fonts/font-library/class-wp-font-collection.php deleted file mode 100644 index 654b23c98497f1..00000000000000 --- a/lib/experimental/fonts/font-library/class-wp-font-collection.php +++ /dev/null @@ -1,216 +0,0 @@ -slug = sanitize_title( $slug ); - $this->name = isset( $args['name'] ) ? $args['name'] : __( 'Unnamed Font Collection', 'gutenberg' ); - $this->description = isset( $args['description'] ) ? $args['description'] : ''; - $this->font_families = isset( $args['font_families'] ) ? $args['font_families'] : array(); - $this->categories = isset( $args['categories'] ) ? $args['categories'] : array(); - - if ( $this->slug !== $slug ) { - _doing_it_wrong( - __METHOD__, - /* translators: %s: Font collection slug. */ - sprintf( __( 'Font collection slug "%s" is not valid. Slugs must use only alphanumeric characters, dashes, and underscores.', 'gutenberg' ), $slug ), - '6.5.0' - ); - } - - if ( empty( $args['font_families'] ) ) { - _doing_it_wrong( - __METHOD__, - /* translators: %s: Font collection slug. */ - sprintf( __( 'Font collection "%s" does not contain any font families.', 'gutenberg' ), $slug ), - '6.5.0' - ); - } - - return true; - } - - /** - * Loads the font collection data from a json file path or url. - * - * @since 6.5.0 - * - * @param string $file_or_url File path or url to a json file containing the font collection data. - * @return array|WP_Error An array containing the font collection data on success, - * else an instance of WP_Error on failure. - */ - public static function load_from_json( $file_or_url ) { - $url = wp_http_validate_url( $file_or_url ); - $file = file_exists( $file_or_url ) ? wp_normalize_path( realpath( $file_or_url ) ) : false; - - if ( ! $url && ! $file ) { - // translators: %s: File path or url to font collection json file. - $message = sprintf( __( 'Font collection JSON file "%s" is invalid or does not exist.', 'gutenberg' ), $file_or_url ); - _doing_it_wrong( __METHOD__, $message, '6.5.0' ); - return new WP_Error( 'font_collection_json_missing', $message ); - } - - return $url ? self::load_from_url( $url ) : self::load_from_file( $file ); - } - - /** - * Loads the font collection data from a json file path. - * - * @since 6.5.0 - * - * @param string $file File path to a json file containing the font collection data. - * @return array|WP_Error An array containing the font collection data on success, - * else an instance of WP_Error on failure. - */ - private static function load_from_file( $file ) { - if ( array_key_exists( $file, static::$collection_json_cache ) ) { - return static::$collection_json_cache[ $file ]; - } - - $data = wp_json_file_decode( $file, array( 'associative' => true ) ); - if ( empty( $data ) ) { - return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection JSON file contents.', 'gutenberg' ) ); - } - - if ( empty( $data['slug'] ) ) { - // translators: %s: Font collection JSON URL. - $message = sprintf( __( 'Font collection JSON file "%s" requires a slug.', 'gutenberg' ), $file ); - _doing_it_wrong( __METHOD__, $message, '6.5.0' ); - return new WP_Error( 'font_collection_invalid_json', $message ); - } - - static::$collection_json_cache[ $file ] = $data; - - return $data; - } - - /** - * Loads the font collection data from a json file url. - * - * @since 6.5.0 - * - * @param string $url Url to a json file containing the font collection data. - * @return array|WP_Error An array containing the font collection data on success, - * else an instance of WP_Error on failure. - */ - private static function load_from_url( $url ) { - if ( array_key_exists( $url, static::$collection_json_cache ) ) { - return static::$collection_json_cache[ $url ]; - } - - // Limit key to 167 characters to avoid failure in the case of a long url. - $transient_key = substr( 'wp_font_collection_url_' . $url, 0, 167 ); - $data = get_site_transient( $transient_key ); - - if ( false === $data ) { - $response = wp_safe_remote_get( $url ); - if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { - // translators: %s: Font collection url. - return new WP_Error( 'font_collection_request_error', sprintf( __( 'Error fetching the font collection data from "%s".', 'gutenberg' ), $url ) ); - } - - $data = json_decode( wp_remote_retrieve_body( $response ), true ); - if ( empty( $data ) ) { - return new WP_Error( 'font_collection_decode_error', __( 'Error decoding the font collection data from the REST response JSON.', 'gutenberg' ) ); - } - - if ( empty( $data['slug'] ) ) { - // translators: %s: Font collection JSON URL. - $message = sprintf( __( 'Font collection JSON file "%s" requires a slug.', 'gutenberg' ), $url ); - _doing_it_wrong( __METHOD__, $message, '6.5.0' ); - return new WP_Error( 'font_collection_invalid_json', $message ); - } - - set_site_transient( $transient_key, $data, DAY_IN_SECONDS ); - } - - static::$collection_json_cache[ $url ] = $data; - - return $data; - } - } -} diff --git a/lib/load.php b/lib/load.php index f5c606727f9f47..4fc9b300606d8f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -137,13 +137,13 @@ function gutenberg_is_experiment_enabled( $name ) { remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler. // Loads the Font Library. -require __DIR__ . '/experimental/fonts/font-library/class-wp-font-collection.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-font-library.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-font-utils.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-families-controller.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-faces-controller.php'; -require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php'; -require __DIR__ . '/experimental/fonts/font-library/font-library.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-collection.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-library.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-utils.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php'; +require __DIR__ . '/compat/wordpress-6.5/fonts/fonts.php'; // Load the Font Face and Font Face Resolver, if not already loaded by WordPress Core. if ( ! class_exists( 'WP_Font_Face' ) ) { @@ -160,18 +160,18 @@ function gutenberg_is_experiment_enabled( $name ) { // Load the BC Layer to avoid fatal errors of extenders using the Fonts API. // @core-merge: do not merge the BC layer files into WordPress Core. -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts-utils.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts-provider-local.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-fonts-resolver.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/webfonts-deprecations.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-webfonts-utils.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-webfonts-provider-local.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-webfonts.php'; -require __DIR__ . '/experimental/fonts/font-face/bc-layer/class-wp-web-fonts.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-provider.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-utils.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-provider-local.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-resolver.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-gutenberg-fonts-api-bc-layer.php'; +require __DIR__ . '/experimental/font-face/bc-layer/webfonts-deprecations.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-webfonts-utils.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-webfonts-provider.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-webfonts-provider-local.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-webfonts.php'; +require __DIR__ . '/experimental/font-face/bc-layer/class-wp-web-fonts.php'; // Plugin specific code. require __DIR__ . '/script-loader.php'; @@ -179,6 +179,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/class-wp-theme-json-data-gutenberg.php'; require __DIR__ . '/class-wp-theme-json-gutenberg.php'; require __DIR__ . '/class-wp-theme-json-resolver-gutenberg.php'; +require __DIR__ . '/class-wp-theme-json-schema-gutenberg.php'; require __DIR__ . '/class-wp-duotone-gutenberg.php'; require __DIR__ . '/blocks.php'; require __DIR__ . '/block-editor-settings.php'; diff --git a/package-lock.json b/package-lock.json index bfb99d54916a2f..e79fd2b4534086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55551,7 +55551,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.111.2", + "version": "1.112.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -55564,7 +55564,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.111.2", + "version": "1.112.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -55575,7 +55575,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.111.2", + "version": "1.112.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/packages/block-editor/src/components/block-list-appender/index.js b/packages/block-editor/src/components/block-list-appender/index.js index 68f36f7dd25058..928f9fc29d1707 100644 --- a/packages/block-editor/src/components/block-list-appender/index.js +++ b/packages/block-editor/src/components/block-list-appender/index.js @@ -39,77 +39,12 @@ function DefaultAppender( { rootClientId } ) { ); } -function useAppender( rootClientId, CustomAppender ) { - const isVisible = useSelect( - ( select ) => { - const { - getTemplateLock, - getSelectedBlockClientId, - __unstableGetEditorMode, - getBlockEditingMode, - } = select( blockEditorStore ); - - if ( ! CustomAppender ) { - const selectedBlockClientId = getSelectedBlockClientId(); - const isParentSelected = - rootClientId === selectedBlockClientId || - ( ! rootClientId && ! selectedBlockClientId ); - if ( ! isParentSelected ) { - return false; - } - } - - if ( - getTemplateLock( rootClientId ) || - getBlockEditingMode( rootClientId ) === 'disabled' || - __unstableGetEditorMode() === 'zoom-out' - ) { - return false; - } - - return true; - }, - [ rootClientId, CustomAppender ] - ); - - if ( ! isVisible ) { - return null; - } - - return CustomAppender ? ( - - ) : ( - - ); -} - -function BlockListAppender( { +export default function BlockListAppender( { rootClientId, - renderAppender, + CustomAppender, className, tagName: TagName = 'div', } ) { - if ( renderAppender === false ) { - return null; - } - - return ( - - ); -} - -function BlockListAppenderInner( { - rootClientId, - renderAppender, - className, - tagName: TagName, -} ) { - const appender = useAppender( rootClientId, renderAppender ); const isDragOver = useSelect( ( select ) => { const { @@ -130,10 +65,6 @@ function BlockListAppenderInner( { [ rootClientId ] ); - if ( ! appender ) { - return null; - } - return ( - { appender } + { CustomAppender ? ( + + ) : ( + + ) } ); } - -export default BlockListAppender; diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 09a4712a3dd6dd..a966c2850289de 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -151,29 +151,51 @@ export default function BlockList( settings ) { function Items( { placeholder, rootClientId, - renderAppender, + renderAppender: CustomAppender, __experimentalAppenderTagName, layout = defaultLayout, } ) { - const { order, selectedBlocks, visibleBlocks, temporarilyEditingAsBlocks } = - useSelect( - ( select ) => { - const { - getBlockOrder, - getSelectedBlockClientIds, - __unstableGetVisibleBlocks, - __unstableGetTemporarilyEditingAsBlocks, - } = select( blockEditorStore ); - return { - order: getBlockOrder( rootClientId ), - selectedBlocks: getSelectedBlockClientIds(), - visibleBlocks: __unstableGetVisibleBlocks(), - temporarilyEditingAsBlocks: - __unstableGetTemporarilyEditingAsBlocks(), - }; - }, - [ rootClientId ] - ); + // Avoid passing CustomAppender to useSelect because it could be a new + // function on every render. + const hasAppender = CustomAppender !== false; + const hasCustomAppender = !! CustomAppender; + const { + order, + selectedBlocks, + visibleBlocks, + temporarilyEditingAsBlocks, + shouldRenderAppender, + } = useSelect( + ( select ) => { + const { + getBlockOrder, + getSelectedBlockClientId, + getSelectedBlockClientIds, + __unstableGetVisibleBlocks, + __unstableGetTemporarilyEditingAsBlocks, + getTemplateLock, + getBlockEditingMode, + __unstableGetEditorMode, + } = select( blockEditorStore ); + const selectedBlockClientId = getSelectedBlockClientId(); + return { + order: getBlockOrder( rootClientId ), + selectedBlocks: getSelectedBlockClientIds(), + visibleBlocks: __unstableGetVisibleBlocks(), + temporarilyEditingAsBlocks: + __unstableGetTemporarilyEditingAsBlocks(), + shouldRenderAppender: + hasAppender && + ( hasCustomAppender + ? ! getTemplateLock( rootClientId ) && + getBlockEditingMode( rootClientId ) !== 'disabled' && + __unstableGetEditorMode() !== 'zoom-out' + : rootClientId === selectedBlockClientId || + ( ! rootClientId && ! selectedBlockClientId ) ), + }; + }, + [ rootClientId, hasAppender, hasCustomAppender ] + ); return ( @@ -199,11 +221,13 @@ function Items( { clientId={ temporarilyEditingAsBlocks } /> ) } - + { shouldRenderAppender && ( + + ) } ); } diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index f73a5de543bd4a..8a8d1908f38544 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -21,7 +21,6 @@ import { copy } from '@wordpress/icons'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import useBlockDisplayInformation from '../use-block-display-information'; import BlockIcon from '../block-icon'; import BlockTransformationsMenu from './block-transformations-menu'; import { useBlockVariationTransforms } from './block-variation-transformations'; @@ -162,7 +161,6 @@ function BlockSwitcherDropdownMenuContents( { } export const BlockSwitcher = ( { clientIds } ) => { - const blockInformation = useBlockDisplayInformation( clientIds?.[ 0 ] ); const { canRemove, hasBlockStyles, @@ -175,9 +173,11 @@ export const BlockSwitcher = ( { clientIds } ) => { const { getBlockRootClientId, getBlocksByClientId, + getBlockAttributes, canRemoveBlocks, } = select( blockEditorStore ); - const { getBlockStyles, getBlockType } = select( blocksStore ); + const { getBlockStyles, getBlockType, getActiveBlockVariation } = + select( blocksStore ); const _blocks = getBlocksByClientId( clientIds ); if ( ! _blocks.length || _blocks.some( ( block ) => ! block ) ) { return { invalidBlocks: true }; @@ -185,18 +185,24 @@ export const BlockSwitcher = ( { clientIds } ) => { const rootClientId = getBlockRootClientId( clientIds ); const [ { name: firstBlockName } ] = _blocks; const _isSingleBlockSelected = _blocks.length === 1; + const blockType = getBlockType( firstBlockName ); + let _icon; if ( _isSingleBlockSelected ) { - _icon = blockInformation?.icon; // Take into account active block variations. + const match = getActiveBlockVariation( + firstBlockName, + getBlockAttributes( clientIds[ 0 ] ) + ); + // Take into account active block variations. + _icon = match?.icon || blockType.icon; } else { const isSelectionOfSameType = new Set( _blocks.map( ( { name } ) => name ) ).size === 1; // When selection consists of blocks of multiple types, display an // appropriate icon to communicate the non-uniformity. - _icon = isSelectionOfSameType - ? getBlockType( firstBlockName )?.icon - : copy; + _icon = isSelectionOfSameType ? blockType.icon : copy; } + return { canRemove: canRemoveBlocks( clientIds, rootClientId ), hasBlockStyles: @@ -209,7 +215,7 @@ export const BlockSwitcher = ( { clientIds } ) => { _isSingleBlockSelected && isTemplatePart( _blocks[ 0 ] ), }; }, - [ clientIds, blockInformation?.icon ] + [ clientIds ] ); const blockTitle = useBlockDisplayTitle( { clientId: clientIds?.[ 0 ], diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 5233c063ba2094..76f3d70041464f 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useMergeRefs } from '@wordpress/compose'; -import { forwardRef, useMemo } from '@wordpress/element'; +import { forwardRef, useMemo, memo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { getBlockSupport, @@ -42,6 +42,8 @@ function BlockContext( { children, clientId } ) { ); } +const BlockListItemsMemo = memo( BlockListItems ); + /** * InnerBlocks is a component which allows a single block to have multiple blocks * as children. The UncontrolledInnerBlocks component is used whenever the inner @@ -117,8 +119,10 @@ function UncontrolledInnerBlocks( props ) { [ defaultLayout, usedLayout, allowSizingOnChildren ] ); + // For controlled inner blocks, we don't want a change in blocks to + // re-render the blocks list. const items = ( - { return getBlockOrder( state, clientId ).every( isChildSubtreeDisabled ); }; +function getEnabledClientIdsTreeUnmemoized( state, rootClientId ) { + const blockOrder = getBlockOrder( state, rootClientId ); + const result = []; + + for ( const clientId of blockOrder ) { + const innerBlocks = getEnabledClientIdsTreeUnmemoized( + state, + clientId + ); + if ( getBlockEditingMode( state, clientId ) !== 'disabled' ) { + result.push( { clientId, innerBlocks } ); + } else { + result.push( ...innerBlocks ); + } + } + + return result; +} + /** * Returns a tree of block objects with only clientID and innerBlocks set. * Blocks with a 'disabled' editing mode are not included. @@ -81,19 +100,7 @@ export const isBlockSubtreeDisabled = ( state, clientId ) => { * @return {Object[]} Tree of block objects with only clientID and innerBlocks set. */ export const getEnabledClientIdsTree = createSelector( - ( state, rootClientId = '' ) => { - return getBlockOrder( state, rootClientId ).flatMap( ( clientId ) => { - if ( getBlockEditingMode( state, clientId ) !== 'disabled' ) { - return [ - { - clientId, - innerBlocks: getEnabledClientIdsTree( state, clientId ), - }, - ]; - } - return getEnabledClientIdsTree( state, clientId ); - } ); - }, + getEnabledClientIdsTreeUnmemoized, ( state ) => [ state.blocks.order, state.blockEditingModes, diff --git a/packages/block-library/src/archives/block.json b/packages/block-library/src/archives/block.json index 7e0f5181d2c3dd..e36691f3143c22 100644 --- a/packages/block-library/src/archives/block.json +++ b/packages/block-library/src/archives/block.json @@ -47,6 +47,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-archives-editor" diff --git a/packages/block-library/src/audio/block.json b/packages/block-library/src/audio/block.json index 04df268a74a630..14b44704fb7e8f 100644 --- a/packages/block-library/src/audio/block.json +++ b/packages/block-library/src/audio/block.json @@ -54,6 +54,9 @@ "margin": false, "padding": false } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-audio-editor", diff --git a/packages/block-library/src/avatar/block.json b/packages/block-library/src/avatar/block.json index fa86541b2963f2..f949e60e0ace15 100644 --- a/packages/block-library/src/avatar/block.json +++ b/packages/block-library/src/avatar/block.json @@ -50,6 +50,9 @@ "text": false, "background": false, "__experimentalDuotone": "img" + }, + "interactivity": { + "clientNavigation": true } }, "selectors": { diff --git a/packages/block-library/src/block/block.json b/packages/block-library/src/block/block.json index 7eac2b705ba1f2..0e5565233ef3c2 100644 --- a/packages/block-library/src/block/block.json +++ b/packages/block-library/src/block/block.json @@ -19,6 +19,9 @@ "customClassName": false, "html": false, "inserter": false, - "renaming": false + "renaming": false, + "interactivity": { + "clientNavigation": true + } } } diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index f04d4642bb98e8..aa5d81c65bad31 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -119,7 +119,10 @@ "width": true } }, - "__experimentalSelector": ".wp-block-button .wp-block-button__link" + "__experimentalSelector": ".wp-block-button .wp-block-button__link", + "interactivity": { + "clientNavigation": true + } }, "styles": [ { "name": "fill", "label": "Fill", "isDefault": true }, diff --git a/packages/block-library/src/buttons/block.json b/packages/block-library/src/buttons/block.json index fde85ae72a316d..015290a4c7cc58 100644 --- a/packages/block-library/src/buttons/block.json +++ b/packages/block-library/src/buttons/block.json @@ -39,6 +39,9 @@ "default": { "type": "flex" } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-buttons-editor", diff --git a/packages/block-library/src/calendar/block.json b/packages/block-library/src/calendar/block.json index 974d47ff8ec2ca..6ba8f7d725e6e5 100644 --- a/packages/block-library/src/calendar/block.json +++ b/packages/block-library/src/calendar/block.json @@ -37,6 +37,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-calendar" diff --git a/packages/block-library/src/categories/block.json b/packages/block-library/src/categories/block.json index 5014da82980493..820ac8945f50f1 100644 --- a/packages/block-library/src/categories/block.json +++ b/packages/block-library/src/categories/block.json @@ -51,6 +51,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-categories-editor", diff --git a/packages/block-library/src/code/block.json b/packages/block-library/src/code/block.json index bd5db3c918b963..4465c8554fc125 100644 --- a/packages/block-library/src/code/block.json +++ b/packages/block-library/src/code/block.json @@ -56,6 +56,9 @@ "background": true, "text": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-code" diff --git a/packages/block-library/src/column/block.json b/packages/block-library/src/column/block.json index 7f61f307fab159..40444d04b93e7c 100644 --- a/packages/block-library/src/column/block.json +++ b/packages/block-library/src/column/block.json @@ -68,6 +68,9 @@ "fontSize": true } }, - "layout": true + "layout": true, + "interactivity": { + "clientNavigation": true + } } } diff --git a/packages/block-library/src/columns/block.json b/packages/block-library/src/columns/block.json index a35e6a5e284893..22db5b1b4ac239 100644 --- a/packages/block-library/src/columns/block.json +++ b/packages/block-library/src/columns/block.json @@ -79,6 +79,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-columns-editor", diff --git a/packages/block-library/src/comment-author-avatar/block.json b/packages/block-library/src/comment-author-avatar/block.json index 050d87c3b4634a..c74b057db2ea3a 100644 --- a/packages/block-library/src/comment-author-avatar/block.json +++ b/packages/block-library/src/comment-author-avatar/block.json @@ -39,6 +39,9 @@ "__experimentalSkipSerialization": true, "margin": true, "padding": true + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/comment-author-name/block.json b/packages/block-library/src/comment-author-name/block.json index 93350779fc8bd2..f3422faf0264df 100644 --- a/packages/block-library/src/comment-author-name/block.json +++ b/packages/block-library/src/comment-author-name/block.json @@ -48,6 +48,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/comment-date/block.json b/packages/block-library/src/comment-date/block.json index 7e4776c68ff2fa..ddc0281e6c668c 100644 --- a/packages/block-library/src/comment-date/block.json +++ b/packages/block-library/src/comment-date/block.json @@ -44,6 +44,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/comment-edit-link/block.json b/packages/block-library/src/comment-edit-link/block.json index 505305f60987ac..a49f9a23161b80 100644 --- a/packages/block-library/src/comment-edit-link/block.json +++ b/packages/block-library/src/comment-edit-link/block.json @@ -44,6 +44,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/comment-template/block.json b/packages/block-library/src/comment-template/block.json index 7b9bfc5e0340fd..70238c45a3d929 100644 --- a/packages/block-library/src/comment-template/block.json +++ b/packages/block-library/src/comment-template/block.json @@ -28,6 +28,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-comment-template" diff --git a/packages/block-library/src/comments-pagination-next/block.json b/packages/block-library/src/comments-pagination-next/block.json index d619865ebd6f5e..22e20bfa8dbf2d 100644 --- a/packages/block-library/src/comments-pagination-next/block.json +++ b/packages/block-library/src/comments-pagination-next/block.json @@ -35,6 +35,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/comments-pagination-numbers/block.json b/packages/block-library/src/comments-pagination-numbers/block.json index fcebb52763870e..9e9017af631978 100644 --- a/packages/block-library/src/comments-pagination-numbers/block.json +++ b/packages/block-library/src/comments-pagination-numbers/block.json @@ -30,6 +30,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/comments-pagination-previous/block.json b/packages/block-library/src/comments-pagination-previous/block.json index 2dab1e9dd73674..0871b000c569dd 100644 --- a/packages/block-library/src/comments-pagination-previous/block.json +++ b/packages/block-library/src/comments-pagination-previous/block.json @@ -35,6 +35,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/comments-pagination/block.json b/packages/block-library/src/comments-pagination/block.json index e4a1a3c3a15d97..28f6c9fdfdb5d9 100644 --- a/packages/block-library/src/comments-pagination/block.json +++ b/packages/block-library/src/comments-pagination/block.json @@ -53,6 +53,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-comments-pagination-editor", diff --git a/packages/block-library/src/comments-title/block.json b/packages/block-library/src/comments-title/block.json index 4107f5d590cdee..f8a02f2e5089b5 100644 --- a/packages/block-library/src/comments-title/block.json +++ b/packages/block-library/src/comments-title/block.json @@ -61,6 +61,9 @@ "__experimentalFontStyle": true, "__experimentalFontWeight": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/cover/block.json b/packages/block-library/src/cover/block.json index 80562da3098997..eb55a8dbabec1c 100644 --- a/packages/block-library/src/cover/block.json +++ b/packages/block-library/src/cover/block.json @@ -132,6 +132,9 @@ }, "layout": { "allowJustification": false + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-cover-editor", diff --git a/packages/block-library/src/details/block.json b/packages/block-library/src/details/block.json index a71d3af2a5ed39..868307d6d22a15 100644 --- a/packages/block-library/src/details/block.json +++ b/packages/block-library/src/details/block.json @@ -58,6 +58,9 @@ }, "layout": { "allowEditing": false + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-details-editor", diff --git a/packages/block-library/src/embed/block.json b/packages/block-library/src/embed/block.json index 5aac8bbd6b8cab..a42aafbab4b0b9 100644 --- a/packages/block-library/src/embed/block.json +++ b/packages/block-library/src/embed/block.json @@ -44,6 +44,9 @@ "align": true, "spacing": { "margin": true + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-embed-editor", diff --git a/packages/block-library/src/footnotes/block.json b/packages/block-library/src/footnotes/block.json index 3192df77969781..1fe74abb471ee5 100644 --- a/packages/block-library/src/footnotes/block.json +++ b/packages/block-library/src/footnotes/block.json @@ -55,6 +55,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-footnotes" diff --git a/packages/block-library/src/gallery/block.json b/packages/block-library/src/gallery/block.json index bfeeb792a7fa02..e9018704bf6bf2 100644 --- a/packages/block-library/src/gallery/block.json +++ b/packages/block-library/src/gallery/block.json @@ -137,6 +137,9 @@ "default": { "type": "flex" } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-gallery-editor", diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index df59c25a7751fb..db7d09c55d2c0d 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -87,6 +87,9 @@ }, "layout": { "allowSizingOnChildren": true + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-group-editor", diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index a1eb3fce32ef15..90ef0d383af2c5 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -61,7 +61,10 @@ } }, "__unstablePasteTextInline": true, - "__experimentalSlashInserter": true + "__experimentalSlashInserter": true, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-heading-editor", "style": "wp-block-heading" diff --git a/packages/block-library/src/home-link/block.json b/packages/block-library/src/home-link/block.json index a9827b7718b9be..b19fcf43ce6880 100644 --- a/packages/block-library/src/home-link/block.json +++ b/packages/block-library/src/home-link/block.json @@ -36,6 +36,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-home-link-editor", diff --git a/packages/block-library/src/html/block.json b/packages/block-library/src/html/block.json index b1a2ad60625d12..08587f69bbd460 100644 --- a/packages/block-library/src/html/block.json +++ b/packages/block-library/src/html/block.json @@ -16,7 +16,10 @@ "supports": { "customClassName": false, "className": false, - "html": false + "html": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-html-editor" } diff --git a/packages/block-library/src/latest-comments/block.json b/packages/block-library/src/latest-comments/block.json index 0b213e9b7903a4..aee42f1d15b486 100644 --- a/packages/block-library/src/latest-comments/block.json +++ b/packages/block-library/src/latest-comments/block.json @@ -46,6 +46,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-latest-comments-editor", diff --git a/packages/block-library/src/latest-posts/block.json b/packages/block-library/src/latest-posts/block.json index f36164614dd506..bb8c2d24962f3f 100644 --- a/packages/block-library/src/latest-posts/block.json +++ b/packages/block-library/src/latest-posts/block.json @@ -110,6 +110,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-latest-posts-editor", diff --git a/packages/block-library/src/list-item/block.json b/packages/block-library/src/list-item/block.json index 0857aaac45d9a2..61c6eec4bb26f4 100644 --- a/packages/block-library/src/list-item/block.json +++ b/packages/block-library/src/list-item/block.json @@ -42,6 +42,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/list/block.json b/packages/block-library/src/list/block.json index a7dcf36cf4ad74..7d2de9567cc54a 100644 --- a/packages/block-library/src/list/block.json +++ b/packages/block-library/src/list/block.json @@ -71,7 +71,10 @@ "__unstablePasteTextInline": true, "__experimentalSelector": "ol,ul", "__experimentalOnMerge": true, - "__experimentalSlashInserter": true + "__experimentalSlashInserter": true, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-list-editor", "style": "wp-block-list" diff --git a/packages/block-library/src/loginout/block.json b/packages/block-library/src/loginout/block.json index 59fceec596e37c..86633003432539 100644 --- a/packages/block-library/src/loginout/block.json +++ b/packages/block-library/src/loginout/block.json @@ -39,6 +39,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/media-text/block.json b/packages/block-library/src/media-text/block.json index cdeb4ce13e8f51..e320101bbd8475 100644 --- a/packages/block-library/src/media-text/block.json +++ b/packages/block-library/src/media-text/block.json @@ -123,6 +123,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-media-text-editor", diff --git a/packages/block-library/src/missing/block.json b/packages/block-library/src/missing/block.json index 242a1d2c6b21a8..f9b3efe594753e 100644 --- a/packages/block-library/src/missing/block.json +++ b/packages/block-library/src/missing/block.json @@ -23,6 +23,9 @@ "customClassName": false, "inserter": false, "html": false, - "reusable": false + "reusable": false, + "interactivity": { + "clientNavigation": true + } } } diff --git a/packages/block-library/src/more/block.json b/packages/block-library/src/more/block.json index bfd95652ea1767..b071f6ba5d7ef7 100644 --- a/packages/block-library/src/more/block.json +++ b/packages/block-library/src/more/block.json @@ -20,7 +20,10 @@ "customClassName": false, "className": false, "html": false, - "multiple": false + "multiple": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-more-editor" } diff --git a/packages/block-library/src/navigation-link/block.json b/packages/block-library/src/navigation-link/block.json index 94a11217d51393..2762bd24e446a6 100644 --- a/packages/block-library/src/navigation-link/block.json +++ b/packages/block-library/src/navigation-link/block.json @@ -77,7 +77,10 @@ "fontSize": true } }, - "renaming": false + "renaming": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-navigation-link-editor", "style": "wp-block-navigation-link" diff --git a/packages/block-library/src/navigation-link/index.php b/packages/block-library/src/navigation-link/index.php index 749bae7aec64e4..9a1421a663fb78 100644 --- a/packages/block-library/src/navigation-link/index.php +++ b/packages/block-library/src/navigation-link/index.php @@ -375,12 +375,11 @@ function block_core_navigation_link_unregister_variation( $name ) { } /** - * Register the navigation link block. * Returns an array of variations for the navigation link block. * * @return array */ -function build_navigation_link_block_variations() { +function block_core_navigation_link_build_variations() { // This will only handle post types and taxonomies registered until this point (init on priority 9). // See action hooks below for other post types and taxonomies. // See https://github.com/WordPress/gutenberg/issues/53826 for details. @@ -429,7 +428,7 @@ function register_block_core_navigation_link() { __DIR__ . '/navigation-link', array( 'render_callback' => 'render_block_core_navigation_link', - 'variation_callback' => 'build_navigation_link_block_variations', + 'variation_callback' => 'block_core_navigation_link_build_variations', ) ); } diff --git a/packages/block-library/src/navigation-submenu/block.json b/packages/block-library/src/navigation-submenu/block.json index 91364109ea7400..0bcf8a1a47f384 100644 --- a/packages/block-library/src/navigation-submenu/block.json +++ b/packages/block-library/src/navigation-submenu/block.json @@ -58,7 +58,10 @@ ], "supports": { "reusable": false, - "html": false + "html": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-navigation-submenu-editor", "style": "wp-block-navigation-submenu" diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index bd8cfebb5bf70f..4fa954b8deb4bc 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -9,6 +9,12 @@ * Helper functions used to render the navigation block. */ class WP_Navigation_Block_Renderer { + + /** + * Used to determine whether or not a navigation has submenus. + */ + private static $has_submenus = false; + /** * Used to determine which blocks are wrapped in an
  • . * @@ -58,22 +64,37 @@ private static function is_responsive( $attributes ) { * Returns whether or not a navigation has a submenu. * * @param WP_Block_List $inner_blocks The list of inner blocks. - * @return bool Returns whether or not a navigation has a submenu. + * @return bool Returns whether or not a navigation has a submenu and also sets the member variable. */ private static function has_submenus( $inner_blocks ) { + if ( true === static::$has_submenus ) { + return static::$has_submenus; + } + foreach ( $inner_blocks as $inner_block ) { - $inner_block_content = $inner_block->render(); - $p = new WP_HTML_Tag_Processor( $inner_block_content ); - if ( $p->next_tag( - array( - 'name' => 'LI', - 'class_name' => 'has-child', - ) - ) ) { - return true; + // If this is a page list then work out if any of the pages have children. + if ( 'core/page-list' === $inner_block->name ) { + $all_pages = get_pages( + array( + 'sort_column' => 'menu_order,post_title', + 'order' => 'asc', + ) + ); + foreach ( (array) $all_pages as $page ) { + if ( $page->post_parent ) { + static::$has_submenus = true; + break; + } + } + } + // If this is a navigation submenu then we know we have submenus. + if ( 'core/navigation-submenu' === $inner_block->name ) { + static::$has_submenus = true; + break; } } - return false; + + return static::$has_submenus; } /** @@ -968,7 +989,9 @@ function block_core_navigation_block_contains_core_navigation( $inner_blocks ) { function block_core_navigation_get_fallback_blocks() { $page_list_fallback = array( array( - 'blockName' => 'core/page-list', + 'blockName' => 'core/page-list', + 'innerContent' => array(), + 'attrs' => array(), ), ); diff --git a/packages/block-library/src/nextpage/block.json b/packages/block-library/src/nextpage/block.json index ab88d4a7be4f0b..3dd1a24d36e59d 100644 --- a/packages/block-library/src/nextpage/block.json +++ b/packages/block-library/src/nextpage/block.json @@ -11,7 +11,10 @@ "supports": { "customClassName": false, "className": false, - "html": false + "html": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-nextpage-editor" } diff --git a/packages/block-library/src/page-list-item/block.json b/packages/block-library/src/page-list-item/block.json index abd86924949ab6..7890771f639219 100644 --- a/packages/block-library/src/page-list-item/block.json +++ b/packages/block-library/src/page-list-item/block.json @@ -45,7 +45,10 @@ "html": false, "lock": false, "inserter": false, - "__experimentalToolbar": false + "__experimentalToolbar": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-page-list-editor", "style": "wp-block-page-list" diff --git a/packages/block-library/src/page-list/block.json b/packages/block-library/src/page-list/block.json index 5035d5611e32e7..b465e4ef5dc3d0 100644 --- a/packages/block-library/src/page-list/block.json +++ b/packages/block-library/src/page-list/block.json @@ -48,6 +48,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-page-list-editor", diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 25a9a36fa8bf0a..2f23d41cd03726 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -64,7 +64,10 @@ } }, "__experimentalSelector": "p", - "__unstablePasteTextInline": true + "__unstablePasteTextInline": true, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-paragraph-editor", "style": "wp-block-paragraph" diff --git a/packages/block-library/src/pattern/block.json b/packages/block-library/src/pattern/block.json index da02f7b72747e4..13fc9d154b15a4 100644 --- a/packages/block-library/src/pattern/block.json +++ b/packages/block-library/src/pattern/block.json @@ -8,7 +8,10 @@ "supports": { "html": false, "inserter": false, - "renaming": false + "renaming": false, + "interactivity": { + "clientNavigation": true + } }, "textdomain": "default", "attributes": { diff --git a/packages/block-library/src/post-author-biography/block.json b/packages/block-library/src/post-author-biography/block.json index 5d7a4d4585747d..8e0ff61c29dfc9 100644 --- a/packages/block-library/src/post-author-biography/block.json +++ b/packages/block-library/src/post-author-biography/block.json @@ -37,6 +37,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/post-author-name/block.json b/packages/block-library/src/post-author-name/block.json index 89e4b38de2c281..7889fed37b9d34 100644 --- a/packages/block-library/src/post-author-name/block.json +++ b/packages/block-library/src/post-author-name/block.json @@ -47,6 +47,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/post-author/block.json b/packages/block-library/src/post-author/block.json index 47dceef55604f6..6f814810744c6d 100644 --- a/packages/block-library/src/post-author/block.json +++ b/packages/block-library/src/post-author/block.json @@ -61,6 +61,9 @@ "background": true, "text": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-post-author" diff --git a/packages/block-library/src/post-comment/block.json b/packages/block-library/src/post-comment/block.json index 558f0e3496eff8..071ef550deb9bc 100644 --- a/packages/block-library/src/post-comment/block.json +++ b/packages/block-library/src/post-comment/block.json @@ -25,6 +25,9 @@ }, "supports": { "html": false, - "inserter": false + "inserter": false, + "interactivity": { + "clientNavigation": true + } } } diff --git a/packages/block-library/src/post-comments-count/block.json b/packages/block-library/src/post-comments-count/block.json index 43c1aaf713776f..796e6e3830236c 100644 --- a/packages/block-library/src/post-comments-count/block.json +++ b/packages/block-library/src/post-comments-count/block.json @@ -38,6 +38,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/post-comments-link/block.json b/packages/block-library/src/post-comments-link/block.json index 74e268c3c20b19..67831b1d15c5d5 100644 --- a/packages/block-library/src/post-comments-link/block.json +++ b/packages/block-library/src/post-comments-link/block.json @@ -39,6 +39,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/post-date/block.json b/packages/block-library/src/post-date/block.json index 11ebc32d9cabec..176e5b6b0ee616 100644 --- a/packages/block-library/src/post-date/block.json +++ b/packages/block-library/src/post-date/block.json @@ -50,6 +50,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/post-excerpt/block.json b/packages/block-library/src/post-excerpt/block.json index 33b7818ebed9f2..4bbc962c9838ef 100644 --- a/packages/block-library/src/post-excerpt/block.json +++ b/packages/block-library/src/post-excerpt/block.json @@ -50,6 +50,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-post-excerpt-editor", diff --git a/packages/block-library/src/post-featured-image/block.json b/packages/block-library/src/post-featured-image/block.json index 4c4ba6919eaff6..75f5bec3ff451d 100644 --- a/packages/block-library/src/post-featured-image/block.json +++ b/packages/block-library/src/post-featured-image/block.json @@ -81,6 +81,9 @@ "spacing": { "margin": true, "padding": true + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-post-featured-image-editor", diff --git a/packages/block-library/src/post-navigation-link/block.json b/packages/block-library/src/post-navigation-link/block.json index cdfecf6c627ca1..ce733759846fee 100644 --- a/packages/block-library/src/post-navigation-link/block.json +++ b/packages/block-library/src/post-navigation-link/block.json @@ -54,6 +54,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-post-navigation-link" diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json index d2f7c09693121c..6a575855583527 100644 --- a/packages/block-library/src/post-template/block.json +++ b/packages/block-library/src/post-template/block.json @@ -48,6 +48,9 @@ "__experimentalDefaultControls": { "blockGap": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-post-template", diff --git a/packages/block-library/src/post-terms/block.json b/packages/block-library/src/post-terms/block.json index 0da7fb02f8134f..538768fd7e5113 100644 --- a/packages/block-library/src/post-terms/block.json +++ b/packages/block-library/src/post-terms/block.json @@ -54,6 +54,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-post-terms" diff --git a/packages/block-library/src/post-terms/index.php b/packages/block-library/src/post-terms/index.php index 44291038383d42..c919db9cda2e4c 100644 --- a/packages/block-library/src/post-terms/index.php +++ b/packages/block-library/src/post-terms/index.php @@ -63,7 +63,7 @@ function render_block_core_post_terms( $attributes, $content, $block ) { * * @return array The available variations for the block. */ -function build_post_term_block_variations() { +function block_core_post_terms_build_variations() { $taxonomies = get_taxonomies( array( 'publicly_queryable' => true, @@ -116,7 +116,7 @@ function register_block_core_post_terms() { __DIR__ . '/post-terms', array( 'render_callback' => 'render_block_core_post_terms', - 'variation_callback' => 'build_post_term_block_variations', + 'variation_callback' => 'block_core_post_terms_build_variations', ) ); } diff --git a/packages/block-library/src/post-time-to-read/block.json b/packages/block-library/src/post-time-to-read/block.json index 281e9bb1f1b210..d19c6e553046c8 100644 --- a/packages/block-library/src/post-time-to-read/block.json +++ b/packages/block-library/src/post-time-to-read/block.json @@ -42,6 +42,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/post-title/block.json b/packages/block-library/src/post-title/block.json index 75a4fa3c3a60fa..b56adecce52b11 100644 --- a/packages/block-library/src/post-title/block.json +++ b/packages/block-library/src/post-title/block.json @@ -57,6 +57,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-post-title" diff --git a/packages/block-library/src/preformatted/block.json b/packages/block-library/src/preformatted/block.json index def870e7ad2fb7..fbec3581bc9d42 100644 --- a/packages/block-library/src/preformatted/block.json +++ b/packages/block-library/src/preformatted/block.json @@ -40,6 +40,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-preformatted" diff --git a/packages/block-library/src/pullquote/block.json b/packages/block-library/src/pullquote/block.json index f041d46cda8cdd..410b477dd9939e 100644 --- a/packages/block-library/src/pullquote/block.json +++ b/packages/block-library/src/pullquote/block.json @@ -69,6 +69,9 @@ "fontSize": "1.5em", "lineHeight": "1.6" } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-pullquote-editor", diff --git a/packages/block-library/src/query-no-results/block.json b/packages/block-library/src/query-no-results/block.json index 32088752bb0606..8f3ba56adcc36a 100644 --- a/packages/block-library/src/query-no-results/block.json +++ b/packages/block-library/src/query-no-results/block.json @@ -28,6 +28,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/query-pagination-next/block.json b/packages/block-library/src/query-pagination-next/block.json index 95b1169dc992fd..ec56125ee3b766 100644 --- a/packages/block-library/src/query-pagination-next/block.json +++ b/packages/block-library/src/query-pagination-next/block.json @@ -41,6 +41,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/query-pagination-numbers/block.json b/packages/block-library/src/query-pagination-numbers/block.json index f22d88115d68cd..8a9f0ee69f14e4 100644 --- a/packages/block-library/src/query-pagination-numbers/block.json +++ b/packages/block-library/src/query-pagination-numbers/block.json @@ -36,6 +36,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-query-pagination-numbers-editor" diff --git a/packages/block-library/src/query-pagination-previous/block.json b/packages/block-library/src/query-pagination-previous/block.json index fbaac543c1da35..d1e34c8630250d 100644 --- a/packages/block-library/src/query-pagination-previous/block.json +++ b/packages/block-library/src/query-pagination-previous/block.json @@ -41,6 +41,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/query-pagination/block.json b/packages/block-library/src/query-pagination/block.json index 973e9486bce6a0..355b188e442d8d 100644 --- a/packages/block-library/src/query-pagination/block.json +++ b/packages/block-library/src/query-pagination/block.json @@ -4,7 +4,7 @@ "name": "core/query-pagination", "title": "Pagination", "category": "theme", - "parent": [ "core/query" ], + "ancestor": [ "core/query" ], "allowedBlocks": [ "core/query-pagination-previous", "core/query-pagination-numbers", @@ -59,6 +59,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-query-pagination-editor", diff --git a/packages/block-library/src/query-title/block.json b/packages/block-library/src/query-title/block.json index 65eb03d310c12d..674daadee3bb69 100644 --- a/packages/block-library/src/query-title/block.json +++ b/packages/block-library/src/query-title/block.json @@ -52,6 +52,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-query-title" diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index 26d194dcd1f23b..b602032df36005 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -47,11 +47,10 @@ "enhancedPagination": "enhancedPagination" }, "supports": { - "interactivity": true, "align": [ "wide", "full" ], "html": false, - "layout": true + "layout": true, + "interactivity": true }, - "editorStyle": "wp-block-query-editor", - "style": "wp-block-query" + "editorStyle": "wp-block-query-editor" } diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 35b3538a725617..a33a1b59f989c9 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -17,7 +17,9 @@ * @return string Returns the modified output of the query block. */ function render_block_core_query( $attributes, $content, $block ) { - $is_interactive = isset( $attributes['enhancedPagination'] ) && true === $attributes['enhancedPagination'] && isset( $attributes['queryId'] ); + $is_interactive = isset( $attributes['enhancedPagination'] ) + && true === $attributes['enhancedPagination'] + && isset( $attributes['queryId'] ); // Enqueue the script module and add the necessary directives if the block is // interactive. @@ -30,43 +32,8 @@ function render_block_core_query( $attributes, $content, $block ) { $p->set_attribute( 'data-wp-interactive', '{"namespace":"core/query"}' ); $p->set_attribute( 'data-wp-router-region', 'query-' . $attributes['queryId'] ); $p->set_attribute( 'data-wp-init', 'callbacks.setQueryRef' ); - // Use context to send translated strings. - $p->set_attribute( - 'data-wp-context', - wp_json_encode( - array( - 'loadingText' => __( 'Loading page, please wait.' ), - 'loadedText' => __( 'Page Loaded.' ), - ), - JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP - ) - ); + $p->set_attribute( 'data-wp-context', '{}' ); $content = $p->get_updated_html(); - - // Mark the block as interactive. - $block->block_type->supports['interactivity'] = true; - - // Add a div to announce messages using `aria-live`. - $html_tag = 'div'; - if ( ! empty( $attributes['tagName'] ) ) { - $html_tag = esc_attr( $attributes['tagName'] ); - } - $last_tag_position = strripos( $content, '' ); - $content = substr_replace( - $content, - '
    -
    ', - $last_tag_position, - 0 - ); } } @@ -137,10 +104,18 @@ function block_core_query_disable_enhanced_pagination( $parsed_block ) { static $dirty_enhanced_queries = array(); static $render_query_callback = null; - $is_interactive = isset( $parsed_block['attrs']['enhancedPagination'] ) && true === $parsed_block['attrs']['enhancedPagination'] && isset( $parsed_block['attrs']['queryId'] ); - $block_name = $parsed_block['blockName']; - - if ( 'core/query' === $block_name && $is_interactive ) { + $block_name = $parsed_block['blockName']; + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_name ); + $has_enhanced_pagination = isset( $parsed_block['attrs']['enhancedPagination'] ) && true === $parsed_block['attrs']['enhancedPagination'] && isset( $parsed_block['attrs']['queryId'] ); + /* + * Client side navigation can be true in two states: + * - supports.interactivity = true; + * - supports.interactivity.clientNavigation = true; + */ + $supports_client_navigation = ( isset( $block_type->supports['interactivity']['clientNavigation'] ) && true === $block_type->supports['interactivity']['clientNavigation'] ) + || ( isset( $block_type->supports['interactivity'] ) && true === $block_type->supports['interactivity'] ); + + if ( 'core/query' === $block_name && $has_enhanced_pagination ) { $enhanced_query_stack[] = $parsed_block['attrs']['queryId']; if ( ! isset( $render_query_callback ) ) { @@ -155,9 +130,9 @@ function block_core_query_disable_enhanced_pagination( $parsed_block ) { * @return string Returns the modified output of the query block. */ $render_query_callback = static function ( $content, $block ) use ( &$enhanced_query_stack, &$dirty_enhanced_queries, &$render_query_callback ) { - $is_interactive = isset( $block['attrs']['enhancedPagination'] ) && true === $block['attrs']['enhancedPagination'] && isset( $block['attrs']['queryId'] ); + $has_enhanced_pagination = isset( $block['attrs']['enhancedPagination'] ) && true === $block['attrs']['enhancedPagination'] && isset( $block['attrs']['queryId'] ); - if ( ! $is_interactive ) { + if ( ! $has_enhanced_pagination ) { return $content; } @@ -185,7 +160,7 @@ function block_core_query_disable_enhanced_pagination( $parsed_block ) { } elseif ( ! empty( $enhanced_query_stack ) && isset( $block_name ) && - ( ! str_starts_with( $block_name, 'core/' ) || 'core/post-content' === $block_name ) + ( ! $supports_client_navigation ) ) { foreach ( $enhanced_query_stack as $query_id ) { $dirty_enhanced_queries[ $query_id ] = true; diff --git a/packages/block-library/src/query/style.scss b/packages/block-library/src/query/style.scss deleted file mode 100644 index 4e9f4741beaed4..00000000000000 --- a/packages/block-library/src/query/style.scss +++ /dev/null @@ -1,52 +0,0 @@ -.wp-block-query__enhanced-pagination-animation { - position: fixed; - top: 0; - left: 0; - margin: 0; - padding: 0; - width: 100vw; - max-width: 100vw !important; - height: 4px; - background-color: var(--wp--preset--color--primary, #000); - opacity: 0; - - &.start-animation { - animation: - wp-block-query__enhanced-pagination-start-animation - 30s - cubic-bezier(0.03, 0.5, 0, 1) - forwards; - } - - &.finish-animation { - animation: - wp-block-query__enhanced-pagination-finish-animation - 300ms - ease-in; - } -} - -@keyframes wp-block-query__enhanced-pagination-start-animation { - 0% { - transform: scaleX(0); - transform-origin: 0% 0%; - opacity: 1; - } - 100% { - transform: scaleX(1); - transform-origin: 0% 0%; - opacity: 1; - } -} - -@keyframes wp-block-query__enhanced-pagination-finish-animation { - 0% { - opacity: 1; - } - 50% { - opacity: 1; - } - 100% { - opacity: 0; - } -} diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js index 85cd16d9e14225..dc82d7968dad45 100644 --- a/packages/block-library/src/query/view.js +++ b/packages/block-library/src/query/view.js @@ -19,14 +19,6 @@ const isValidEvent = ( event ) => ! event.defaultPrevented; store( 'core/query', { - state: { - get startAnimation() { - return getContext().animation === 'start'; - }, - get finishAnimation() { - return getContext().animation === 'finish'; - }, - }, actions: { *navigate( event ) { const ctx = getContext(); @@ -37,28 +29,10 @@ store( 'core/query', { if ( isValidLink( ref ) && isValidEvent( event ) && ! isDisabled ) { event.preventDefault(); - // Don't announce the navigation immediately, wait 400 ms. - const timeout = setTimeout( () => { - ctx.message = ctx.loadingText; - ctx.animation = 'start'; - }, 400 ); - const { actions } = yield import( '@wordpress/interactivity-router' ); yield actions.navigate( ref.href ); - - // Dismiss loading message if it hasn't been added yet. - clearTimeout( timeout ); - - // Announce that the page has been loaded. If the message is the - // same, we use a no-break space similar to the @wordpress/a11y - // package: https://github.com/WordPress/gutenberg/blob/c395242b8e6ee20f8b06c199e4fc2920d7018af1/packages/a11y/src/filter-message.js#L20-L26 - ctx.message = - ctx.loadedText + - ( ctx.message === ctx.loadedText ? '\u00A0' : '' ); - - ctx.animation = 'finish'; ctx.url = ref.href; // Focus the first anchor of the Query block. diff --git a/packages/block-library/src/quote/block.json b/packages/block-library/src/quote/block.json index 9deca000efe06b..b66e64bce2eead 100644 --- a/packages/block-library/src/quote/block.json +++ b/packages/block-library/src/quote/block.json @@ -58,6 +58,9 @@ }, "spacing": { "blockGap": true + }, + "interactivity": { + "clientNavigation": true } }, "styles": [ diff --git a/packages/block-library/src/read-more/block.json b/packages/block-library/src/read-more/block.json index d3386a49d66b82..9e4fc84d193f71 100644 --- a/packages/block-library/src/read-more/block.json +++ b/packages/block-library/src/read-more/block.json @@ -50,6 +50,9 @@ "__experimentalDefaultControls": { "width": true } + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-read-more" diff --git a/packages/block-library/src/rss/block.json b/packages/block-library/src/rss/block.json index 2535eda5946fbd..36d70e7b7ccb98 100644 --- a/packages/block-library/src/rss/block.json +++ b/packages/block-library/src/rss/block.json @@ -43,7 +43,10 @@ }, "supports": { "align": true, - "html": false + "html": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-rss-editor", "style": "wp-block-rss" diff --git a/packages/block-library/src/separator/block.json b/packages/block-library/src/separator/block.json index 970f6b5cbb5821..484627aaa1612a 100644 --- a/packages/block-library/src/separator/block.json +++ b/packages/block-library/src/separator/block.json @@ -28,6 +28,9 @@ }, "spacing": { "margin": [ "top", "bottom" ] + }, + "interactivity": { + "clientNavigation": true } }, "styles": [ diff --git a/packages/block-library/src/site-logo/block.json b/packages/block-library/src/site-logo/block.json index d1e3d1b20c3dad..3bdbdc1b809ab1 100644 --- a/packages/block-library/src/site-logo/block.json +++ b/packages/block-library/src/site-logo/block.json @@ -45,6 +45,9 @@ "margin": false, "padding": false } + }, + "interactivity": { + "clientNavigation": true } }, "styles": [ diff --git a/packages/block-library/src/site-tagline/block.json b/packages/block-library/src/site-tagline/block.json index 22fb59aab5ead7..2361be9ea3efca 100644 --- a/packages/block-library/src/site-tagline/block.json +++ b/packages/block-library/src/site-tagline/block.json @@ -43,6 +43,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-site-tagline-editor" diff --git a/packages/block-library/src/site-title/block.json b/packages/block-library/src/site-title/block.json index 4a2685e6941fc6..6179452cd15b7a 100644 --- a/packages/block-library/src/site-title/block.json +++ b/packages/block-library/src/site-title/block.json @@ -58,6 +58,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-site-title-editor", diff --git a/packages/block-library/src/social-link/block.json b/packages/block-library/src/social-link/block.json index 50e95efedb630c..d487465ef79664 100644 --- a/packages/block-library/src/social-link/block.json +++ b/packages/block-library/src/social-link/block.json @@ -31,7 +31,10 @@ ], "supports": { "reusable": false, - "html": false + "html": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-social-link-editor" } diff --git a/packages/block-library/src/social-links/block.json b/packages/block-library/src/social-links/block.json index 1aea3684d0c63d..0c8c7be1eba9a5 100644 --- a/packages/block-library/src/social-links/block.json +++ b/packages/block-library/src/social-links/block.json @@ -78,6 +78,9 @@ "margin": true, "padding": false } + }, + "interactivity": { + "clientNavigation": true } }, "styles": [ diff --git a/packages/block-library/src/spacer/block.json b/packages/block-library/src/spacer/block.json index a9da8d537f1b61..447ea99cc0b677 100644 --- a/packages/block-library/src/spacer/block.json +++ b/packages/block-library/src/spacer/block.json @@ -23,6 +23,9 @@ "__experimentalDefaultControls": { "margin": true } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-spacer-editor", diff --git a/packages/block-library/src/table-of-contents/block.json b/packages/block-library/src/table-of-contents/block.json index 5fa7e37e6acbdf..7857744d91c4eb 100644 --- a/packages/block-library/src/table-of-contents/block.json +++ b/packages/block-library/src/table-of-contents/block.json @@ -45,6 +45,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } }, "example": {} diff --git a/packages/block-library/src/table/block.json b/packages/block-library/src/table/block.json index 470886a1247fe1..44177ef50a7e20 100644 --- a/packages/block-library/src/table/block.json +++ b/packages/block-library/src/table/block.json @@ -195,7 +195,10 @@ "width": true } }, - "__experimentalSelector": ".wp-block-table > table" + "__experimentalSelector": ".wp-block-table > table", + "interactivity": { + "clientNavigation": true + } }, "styles": [ { diff --git a/packages/block-library/src/tag-cloud/block.json b/packages/block-library/src/tag-cloud/block.json index 9481dc945666ad..b95e02204faa29 100644 --- a/packages/block-library/src/tag-cloud/block.json +++ b/packages/block-library/src/tag-cloud/block.json @@ -48,6 +48,9 @@ "__experimentalFontStyle": true, "__experimentalTextTransform": true, "__experimentalLetterSpacing": true + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-tag-cloud-editor" diff --git a/packages/block-library/src/template-part/block.json b/packages/block-library/src/template-part/block.json index 3b0946718bcb9c..9710bdeee2e535 100644 --- a/packages/block-library/src/template-part/block.json +++ b/packages/block-library/src/template-part/block.json @@ -24,7 +24,10 @@ "align": true, "html": false, "reusable": false, - "renaming": false + "renaming": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-template-part-editor" } diff --git a/packages/block-library/src/term-description/block.json b/packages/block-library/src/term-description/block.json index fc91a4aff4c484..7a3f27c8063ab6 100644 --- a/packages/block-library/src/term-description/block.json +++ b/packages/block-library/src/term-description/block.json @@ -37,6 +37,9 @@ "__experimentalDefaultControls": { "fontSize": true } + }, + "interactivity": { + "clientNavigation": true } } } diff --git a/packages/block-library/src/text-columns/block.json b/packages/block-library/src/text-columns/block.json index 3af169fadbb3bd..2599df111d34bd 100644 --- a/packages/block-library/src/text-columns/block.json +++ b/packages/block-library/src/text-columns/block.json @@ -29,7 +29,10 @@ } }, "supports": { - "inserter": false + "inserter": false, + "interactivity": { + "clientNavigation": true + } }, "editorStyle": "wp-block-text-columns-editor", "style": "wp-block-text-columns" diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index 846a1dc99caafc..1d6b817c003023 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -55,6 +55,9 @@ "width": true, "color": true, "style": true + }, + "interactivity": { + "clientNavigation": true } }, "style": "wp-block-verse", diff --git a/packages/block-library/src/video/block.json b/packages/block-library/src/video/block.json index 5d4680f39e79a8..2bc153bc1c6165 100644 --- a/packages/block-library/src/video/block.json +++ b/packages/block-library/src/video/block.json @@ -88,6 +88,9 @@ "margin": false, "padding": false } + }, + "interactivity": { + "clientNavigation": true } }, "editorStyle": "wp-block-video-editor", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6080498d5e92ba..73b2482587ba3c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,9 +2,13 @@ ## Unreleased + ### Enhancements +- `Button`: Update secondary variant to show the border even in a disabled state. ([#58606](https://github.com/WordPress/gutenberg/pull/58606)). - `ConfirmDialog`: Add `__next40pxDefaultSize` to buttons ([#58421](https://github.com/WordPress/gutenberg/pull/58421)). +- `Snackbar`: Update the warning message ([#58591](https://github.com/WordPress/gutenberg/pull/58591)). +- `Composite`: Implementing `useCompositeState` with Ariakit ([#57304](https://github.com/WordPress/gutenberg/pull/57304)) ### Bug Fix @@ -12,13 +16,24 @@ - `Placeholder`: Fix Placeholder component padding when body text font size is changed ([#58323](https://github.com/WordPress/gutenberg/pull/58323)). - `Placeholder`: Fix Global Styles typography settings bleeding into placeholder component ([#58303](https://github.com/WordPress/gutenberg/pull/58303)). - `PaletteEdit`: Fix palette item accessibility in details view ([#58214](https://github.com/WordPress/gutenberg/pull/58214)). +- `Snackbar`: Fix the auto-dismissal timers ([#58604](https://github.com/WordPress/gutenberg/pull/58604)). ### Experimental - `Guide`, `Modal`: Restore accent color themability ([#58098](https://github.com/WordPress/gutenberg/pull/58098)). - `DropdownMenuV2`: Restore accent color themability ([#58130](https://github.com/WordPress/gutenberg/pull/58130)). +- `Tabs`: improve controlled mode focus handling and keyboard navigation ([#57696](https://github.com/WordPress/gutenberg/pull/57696)). +- `Tabs`: prevent internal focus from updating too early ([#58625](https://github.com/WordPress/gutenberg/pull/58625)). - Expand theming support in the `COLORS` variable object ([#58097](https://github.com/WordPress/gutenberg/pull/58097)). +### Enhancements + +- `CheckboxControl`: Remove ability for label prop to be false ([#58339](https://github.com/WordPress/gutenberg/pull/58339)). + +### Internal + +- `Composite`: Removing Reakit `Composite` implementation ([#58620](https://github.com/WordPress/gutenberg/pull/58620)). + ## 25.16.0 (2024-01-24) ### Enhancements diff --git a/packages/components/src/button/stories/e2e/index.story.tsx b/packages/components/src/button/stories/e2e/index.story.tsx index c2ec8e237d3b2a..1baeb08fb73e23 100644 --- a/packages/components/src/button/stories/e2e/index.story.tsx +++ b/packages/components/src/button/stories/e2e/index.story.tsx @@ -40,6 +40,12 @@ export const VariantStates: StoryFn< typeof Button > = ( > + + + + ); + renderAndValidate( ); + + await press.Tab(); + expect( screen.getByText( 'Before' ) ).toHaveFocus(); + await press.Tab(); + expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); + await press.Tab(); + expect( screen.getByText( 'After' ) ).toHaveFocus(); + await press.ShiftTab(); + expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); + } ); + + test( 'Excludes disabled items', async () => { + const Test = () => { + const props = useProps(); + return ( + + Item 1 + + Item 2 + + Item 3 + + ); + }; + renderAndValidate( ); + + const { item1, item2, item3 } = getOneDimensionalItems(); + + expect( item2 ).toBeDisabled(); + + await press.Tab(); + expect( item1 ).toHaveFocus(); + await press.ArrowDown(); + expect( item2 ).not.toHaveFocus(); + expect( item3 ).toHaveFocus(); + } ); + + test( 'Includes focusable disabled items', async () => { + const Test = () => { + const props = useProps(); + return ( + + Item 1 + + Item 2 + + Item 3 + + ); + }; + renderAndValidate( ); + const { item1, item2, item3 } = getOneDimensionalItems(); + + expect( item2 ).toBeEnabled(); + expect( item2 ).toHaveAttribute( 'aria-disabled', 'true' ); + + await press.Tab(); + expect( item1 ).toHaveFocus(); + await press.ArrowDown(); + expect( item2 ).toHaveFocus(); + expect( item3 ).not.toHaveFocus(); + } ); + + test( 'Supports `baseId`', async () => { + const Test = () => ( + + ); + renderAndValidate( ); + const { item1, item2, item3 } = getOneDimensionalItems(); + + expect( item1.id ).toMatch( 'test-id-1' ); + expect( item2.id ).toMatch( 'test-id-2' ); + expect( item3.id ).toMatch( 'test-id-3' ); + } ); + + test( 'Supports `currentId`', async () => { + const Test = () => ( + + ); + renderAndValidate( ); + const { item2 } = getOneDimensionalItems(); + + await press.Tab(); + expect( item2 ).toHaveFocus(); + } ); +} ); + +describe.each( [ + [ 'When LTR', false ], + [ 'When RTL', true ], +] )( '%s', ( _when, rtl ) => { + const { previous, next, first, last } = getKeys( rtl ); + + function useOneDimensionalTest( initialState?: InitialState ) { + const Test = () => ( + + ); + renderAndValidate( ); + return getOneDimensionalItems(); + } + + function useTwoDimensionalTest( initialState?: InitialState ) { + const Test = () => ( + + ); + renderAndValidate( ); + return getTwoDimensionalItems(); + } + + function useShiftTest( shift: boolean ) { + const Test = () => ( + + ); + renderAndValidate( ); + return getShiftTestItems(); + } + + describe( 'In one dimension', () => { + test( 'All directions work with no orientation', async () => { + const { item1, item2, item3 } = useOneDimensionalTest(); + + await press.Tab(); + expect( item1 ).toHaveFocus(); + await press.ArrowDown(); + expect( item2 ).toHaveFocus(); + await press.ArrowDown(); + expect( item3 ).toHaveFocus(); + await press.ArrowDown(); + expect( item3 ).toHaveFocus(); + await press.ArrowUp(); + expect( item2 ).toHaveFocus(); + await press.ArrowUp(); + expect( item1 ).toHaveFocus(); + await press.ArrowUp(); + expect( item1 ).toHaveFocus(); + await press( next ); + expect( item2 ).toHaveFocus(); + await press( next ); + expect( item3 ).toHaveFocus(); + await press( previous ); + expect( item2 ).toHaveFocus(); + await press( previous ); + expect( item1 ).toHaveFocus(); + await press.End(); + expect( item3 ).toHaveFocus(); + await press.Home(); + expect( item1 ).toHaveFocus(); + await press.PageDown(); + expect( item3 ).toHaveFocus(); + await press.PageUp(); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Only left/right work with horizontal orientation', async () => { + const { item1, item2, item3 } = useOneDimensionalTest( { + orientation: 'horizontal', + } ); + + await press.Tab(); + expect( item1 ).toHaveFocus(); + await press.ArrowDown(); + expect( item1 ).toHaveFocus(); + await press( next ); + expect( item2 ).toHaveFocus(); + await press( next ); + expect( item3 ).toHaveFocus(); + await press.ArrowUp(); + expect( item3 ).toHaveFocus(); + await press( previous ); + expect( item2 ).toHaveFocus(); + await press( previous ); + expect( item1 ).toHaveFocus(); + await press.End(); + expect( item3 ).toHaveFocus(); + await press.Home(); + expect( item1 ).toHaveFocus(); + await press.PageDown(); + expect( item3 ).toHaveFocus(); + await press.PageUp(); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Only up/down work with vertical orientation', async () => { + const { item1, item2, item3 } = useOneDimensionalTest( { + orientation: 'vertical', + } ); + + await press.Tab(); + expect( item1 ).toHaveFocus(); + await press( next ); + expect( item1 ).toHaveFocus(); + await press.ArrowDown(); + expect( item2 ).toHaveFocus(); + await press.ArrowDown(); + expect( item3 ).toHaveFocus(); + await press( previous ); + expect( item3 ).toHaveFocus(); + await press.ArrowUp(); + expect( item2 ).toHaveFocus(); + await press.ArrowUp(); + expect( item1 ).toHaveFocus(); + await press.End(); + expect( item3 ).toHaveFocus(); + await press.Home(); + expect( item1 ).toHaveFocus(); + await press.PageDown(); + expect( item3 ).toHaveFocus(); + await press.PageUp(); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Focus wraps with loop enabled', async () => { + const { item1, item2, item3 } = useOneDimensionalTest( { + loop: true, + } ); + + await press.Tab(); + expect( item1 ).toHaveFocus(); + await press.ArrowDown(); + expect( item2 ).toHaveFocus(); + await press.ArrowDown(); + expect( item3 ).toHaveFocus(); + await press.ArrowDown(); + expect( item1 ).toHaveFocus(); + await press.ArrowUp(); + expect( item3 ).toHaveFocus(); + await press( next ); + expect( item1 ).toHaveFocus(); + await press( previous ); + expect( item3 ).toHaveFocus(); + } ); + } ); + + describe( 'In two dimensions', () => { + test( 'All directions work as standard', async () => { + const { itemA1, itemA2, itemA3, itemB1, itemB2, itemC1, itemC3 } = + useTwoDimensionalTest(); + + await press.Tab(); + expect( itemA1 ).toHaveFocus(); + await press.ArrowUp(); + expect( itemA1 ).toHaveFocus(); + await press( previous ); + expect( itemA1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemB1 ).toHaveFocus(); + await press( next ); + expect( itemB2 ).toHaveFocus(); + await press.ArrowUp(); + expect( itemA2 ).toHaveFocus(); + await press( previous ); + expect( itemA1 ).toHaveFocus(); + await press( last ); + expect( itemA3 ).toHaveFocus(); + await press.PageDown(); + expect( itemC3 ).toHaveFocus(); + await press( next ); + expect( itemC3 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemC3 ).toHaveFocus(); + await press( first ); + expect( itemC1 ).toHaveFocus(); + await press.PageUp(); + expect( itemA1 ).toHaveFocus(); + await press.End( null, { ctrlKey: true } ); + expect( itemC3 ).toHaveFocus(); + await press.Home( null, { ctrlKey: true } ); + expect( itemA1 ).toHaveFocus(); + } ); + + test( 'Focus wraps around rows/columns with loop enabled', async () => { + const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = + useTwoDimensionalTest( { loop: true } ); + + await press.Tab(); + expect( itemA1 ).toHaveFocus(); + await press( next ); + expect( itemA2 ).toHaveFocus(); + await press( next ); + expect( itemA3 ).toHaveFocus(); + await press( next ); + expect( itemA1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemB1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemC1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemA1 ).toHaveFocus(); + await press( previous ); + expect( itemA3 ).toHaveFocus(); + await press.ArrowUp(); + expect( itemC3 ).toHaveFocus(); + } ); + + test( 'Focus moves between rows/columns with wrap enabled', async () => { + const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = + useTwoDimensionalTest( { wrap: true } ); + + await press.Tab(); + expect( itemA1 ).toHaveFocus(); + await press( next ); + expect( itemA2 ).toHaveFocus(); + await press( next ); + expect( itemA3 ).toHaveFocus(); + await press( next ); + expect( itemB1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemC1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemA2 ).toHaveFocus(); + await press( previous ); + expect( itemA1 ).toHaveFocus(); + await press( previous ); + expect( itemA1 ).toHaveFocus(); + await press.ArrowUp(); + expect( itemA1 ).toHaveFocus(); + await press.End( itemA1, { ctrlKey: true } ); + expect( itemC3 ).toHaveFocus(); + await press( next ); + expect( itemC3 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemC3 ).toHaveFocus(); + } ); + + test( 'Focus wraps around start/end with loop and wrap enabled', async () => { + const { itemA1, itemC3 } = useTwoDimensionalTest( { + loop: true, + wrap: true, + } ); + + await press.Tab(); + expect( itemA1 ).toHaveFocus(); + await press( previous ); + expect( itemC3 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemA1 ).toHaveFocus(); + await press.ArrowUp(); + expect( itemC3 ).toHaveFocus(); + await press( next ); + expect( itemA1 ).toHaveFocus(); + } ); + + test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { + const { itemA1, itemB1, itemB2, itemC1 } = useShiftTest( true ); + + await press.Tab(); + expect( itemA1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemB1 ).toHaveFocus(); + await press( next ); + expect( itemB2 ).toHaveFocus(); + await press.ArrowUp(); + // A2 doesn't exist + expect( itemA1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemB1 ).toHaveFocus(); + await press( next ); + expect( itemB2 ).toHaveFocus(); + await press.ArrowDown(); + // C2 is disabled + expect( itemC1 ).toHaveFocus(); + } ); + + test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { + const { itemA1, itemB1, itemB2 } = useShiftTest( false ); + + await press.Tab(); + expect( itemA1 ).toHaveFocus(); + await press.ArrowDown(); + expect( itemB1 ).toHaveFocus(); + await press( next ); + expect( itemB2 ).toHaveFocus(); + await press.ArrowUp(); + // A2 doesn't exist + expect( itemB2 ).toHaveFocus(); + await press.ArrowDown(); + // C2 is disabled + expect( itemB2 ).toHaveFocus(); + } ); + } ); +} ); diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx deleted file mode 100644 index 02fe6c3d1d60ab..00000000000000 --- a/packages/components/src/composite/test/index.tsx +++ /dev/null @@ -1,576 +0,0 @@ -/** - * External dependencies - */ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import { - Composite as ReakitComposite, - CompositeGroup as ReakitCompositeGroup, - CompositeItem as ReakitCompositeItem, - useCompositeState as ReakitUseCompositeState, -} from '..'; - -const COMPOSITE_SUITES = { - reakit: { - Composite: ReakitComposite, - CompositeGroup: ReakitCompositeGroup, - CompositeItem: ReakitCompositeItem, - useCompositeState: ReakitUseCompositeState, - }, -}; - -type InitialState = Parameters< typeof ReakitUseCompositeState >[ 0 ]; - -// It was decided not to test the full API, instead opting -// to cover basic usage, with a view to adding broader support -// for the original API should the need arise. As such we are -// only testing here for standard usage. -// See https://github.com/WordPress/gutenberg/pull/56645 - -describe.each( Object.entries( COMPOSITE_SUITES ) )( - 'Validate %s implementation', - ( _, { Composite, CompositeGroup, CompositeItem, useCompositeState } ) => { - function useSpreadProps( initialState?: InitialState ) { - return useCompositeState( initialState ); - } - - function useStateProps( initialState?: InitialState ) { - return { - state: useCompositeState( initialState ), - }; - } - - function OneDimensionalTest( { ...props } ) { - return ( - - Item 1 - Item 2 - Item 3 - - ); - } - - function getOneDimensionalItems() { - return { - item1: screen.getByText( 'Item 1' ), - item2: screen.getByText( 'Item 2' ), - item3: screen.getByText( 'Item 3' ), - }; - } - - function TwoDimensionalTest( { ...props } ) { - return ( - - - Item A1 - Item A2 - Item A3 - - - Item B1 - Item B2 - Item B3 - - - Item C1 - Item C2 - Item C3 - - - ); - } - - function getTwoDimensionalItems() { - return { - itemA1: screen.getByText( 'Item A1' ), - itemA2: screen.getByText( 'Item A2' ), - itemA3: screen.getByText( 'Item A3' ), - itemB1: screen.getByText( 'Item B1' ), - itemB2: screen.getByText( 'Item B2' ), - itemB3: screen.getByText( 'Item B3' ), - itemC1: screen.getByText( 'Item C1' ), - itemC2: screen.getByText( 'Item C2' ), - itemC3: screen.getByText( 'Item C3' ), - }; - } - - function ShiftTest( { ...props } ) { - return ( - - - Item A1 - - - Item B1 - Item B2 - - - Item C1 - - Item C2 - - - - ); - } - - function getShiftTestItems() { - return { - itemA1: screen.getByText( 'Item A1' ), - itemB1: screen.getByText( 'Item B1' ), - itemB2: screen.getByText( 'Item B2' ), - itemC1: screen.getByText( 'Item C1' ), - itemC2: screen.getByText( 'Item C2' ), - }; - } - - describe.each( [ - [ 'With spread state', useSpreadProps ], - [ 'With `state` prop', useStateProps ], - ] )( '%s', ( __, useProps ) => { - function useOneDimensionalTest( initialState?: InitialState ) { - const Test = () => ( - - ); - render( ); - return getOneDimensionalItems(); - } - - test( 'Renders as a single tab stop', async () => { - const user = userEvent.setup(); - const Test = () => ( - <> - - - - - ); - render( ); - - await user.tab(); - expect( screen.getByText( 'Before' ) ).toHaveFocus(); - await user.tab(); - expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); - await user.tab(); - expect( screen.getByText( 'After' ) ).toHaveFocus(); - await user.tab( { shift: true } ); - expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); - } ); - - test( 'Excludes disabled items', async () => { - const user = userEvent.setup(); - const Test = () => { - const props = useProps(); - return ( - - Item 1 - - Item 2 - - Item 3 - - ); - }; - render( ); - - const { item1, item2, item3 } = getOneDimensionalItems(); - - expect( item2 ).toBeDisabled(); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).not.toHaveFocus(); - expect( item3 ).toHaveFocus(); - } ); - - test( 'Includes focusable disabled items', async () => { - const user = userEvent.setup(); - const Test = () => { - const props = useProps(); - return ( - - Item 1 - - Item 2 - - Item 3 - - ); - }; - render( ); - const { item1, item2, item3 } = getOneDimensionalItems(); - - expect( item2 ).toBeEnabled(); - expect( item2 ).toHaveAttribute( 'aria-disabled', 'true' ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - expect( item3 ).not.toHaveFocus(); - } ); - - test( 'Supports `baseId`', async () => { - const { item1, item2, item3 } = useOneDimensionalTest( { - baseId: 'test-id', - } ); - - expect( item1.id ).toMatch( 'test-id-1' ); - expect( item2.id ).toMatch( 'test-id-2' ); - expect( item3.id ).toMatch( 'test-id-3' ); - } ); - - test( 'Supports `currentId`', async () => { - const user = userEvent.setup(); - const { item2 } = useOneDimensionalTest( { - baseId: 'test-id', - currentId: 'test-id-2', - } ); - - await user.tab(); - expect( item2 ).toHaveFocus(); - } ); - } ); - - describe.each( [ - [ - 'When LTR', - false, - { previous: 'ArrowLeft', next: 'ArrowRight' }, - ], - [ 'When RTL', true, { previous: 'ArrowRight', next: 'ArrowLeft' } ], - ] )( '%s', ( _when, rtl, { previous, next } ) => { - function useOneDimensionalTest( initialState?: InitialState ) { - const Test = () => ( - - ); - render( ); - return getOneDimensionalItems(); - } - - function useTwoDimensionalTest( initialState?: InitialState ) { - const Test = () => ( - - ); - render( ); - return getTwoDimensionalItems(); - } - - function useShiftTest( shift: boolean ) { - const Test = () => ( - - ); - render( ); - return getShiftTestItems(); - } - - describe( 'In one dimension', () => { - test( 'All directions work with no orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = useOneDimensionalTest( { - rtl, - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item2 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item3 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item2 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); - } ); - - test( 'Only left/right work with horizontal orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = useOneDimensionalTest( { - rtl, - orientation: 'horizontal', - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item2 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item2 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); - } ); - - test( 'Only up/down work with vertical orientation', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = useOneDimensionalTest( { - rtl, - orientation: 'vertical', - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( item1 ).toHaveFocus(); - } ); - - test( 'Focus wraps with loop enabled', async () => { - const user = userEvent.setup(); - const { item1, item2, item3 } = useOneDimensionalTest( { - rtl, - loop: true, - } ); - - await user.tab(); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( item1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( item3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( item1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( item3 ).toHaveFocus(); - } ); - } ); - - describe( 'In two dimensions', () => { - test( 'All directions work as standard', async () => { - const user = userEvent.setup(); - const { - itemA1, - itemA2, - itemA3, - itemB1, - itemB2, - itemC1, - itemC3, - } = useTwoDimensionalTest( { rtl } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[End]' ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( '[PageDown]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[Home]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[PageUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '{Control>}[End]{/Control}' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '{Control>}[Home]{/Control}' ); - expect( itemA1 ).toHaveFocus(); - } ); - - test( 'Focus wraps around rows/columns with loop enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = - useTwoDimensionalTest( { rtl, loop: true } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemC3 ).toHaveFocus(); - } ); - - test( 'Focus moves between rows/columns with wrap enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = - useTwoDimensionalTest( { rtl, wrap: true } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA2 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '{Control>}[End]{/Control}' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemC3 ).toHaveFocus(); - } ); - - test( 'Focus wraps around start/end with loop and wrap enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemC3 } = useTwoDimensionalTest( { - rtl, - loop: true, - wrap: true, - } ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( `[${ previous }]` ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - expect( itemC3 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemA1 ).toHaveFocus(); - } ); - - test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemB1, itemB2, itemC1 } = - useShiftTest( true ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - // A2 doesn't exist - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - // C2 is disabled - expect( itemC1 ).toHaveFocus(); - } ); - - test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { - const user = userEvent.setup(); - const { itemA1, itemB1, itemB2 } = useShiftTest( false ); - - await user.tab(); - expect( itemA1 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - expect( itemB1 ).toHaveFocus(); - await user.keyboard( `[${ next }]` ); - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowUp]' ); - // A2 doesn't exist - expect( itemB2 ).toHaveFocus(); - await user.keyboard( '[ArrowDown]' ); - // C2 is disabled - expect( itemB2 ).toHaveFocus(); - } ); - } ); - } ); - } -); diff --git a/packages/components/src/composite/v2.ts b/packages/components/src/composite/v2.ts index d329fd3fd11dfb..5e3e8c13fd05e7 100644 --- a/packages/components/src/composite/v2.ts +++ b/packages/components/src/composite/v2.ts @@ -1,22 +1,4 @@ -/** - * Composite is a component that may contain navigable items represented by - * CompositeItem. It's inspired by the WAI-ARIA Composite Role and implements - * all the keyboard navigation mechanisms to ensure that there's only one - * tab stop for the whole Composite element. This means that it can behave as - * a roving tabindex or aria-activedescendant container. - * - * @see https://ariakit.org/components/composite - */ +// Until we migrate away from Reakit, the 'current' +// Ariakit implementation is considered a v2. -/* eslint-disable-next-line no-restricted-imports */ -export { - Composite, - CompositeGroup, - CompositeGroupLabel, - CompositeItem, - CompositeRow, - useCompositeStore, -} from '@ariakit/react'; - -/* eslint-disable-next-line no-restricted-imports */ -export type { CompositeStore } from '@ariakit/react'; +export * from './current'; diff --git a/packages/components/src/snackbar/index.tsx b/packages/components/src/snackbar/index.tsx index 45f1800bb2249f..48eb8ed4873801 100644 --- a/packages/components/src/snackbar/index.tsx +++ b/packages/components/src/snackbar/index.tsx @@ -8,7 +8,13 @@ import classnames from 'classnames'; * WordPress dependencies */ import { speak } from '@wordpress/a11y'; -import { useEffect, forwardRef, renderToString } from '@wordpress/element'; +import { + useEffect, + useLayoutEffect, + useRef, + forwardRef, + renderToString, +} from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import warning from '@wordpress/warning'; @@ -88,17 +94,24 @@ function UnforwardedSnackbar( useSpokenMessage( spokenMessage, politeness ); - // Only set up the timeout dismiss if we're not explicitly dismissing. + // The `onDismiss/onRemove` can have unstable references, + // trigger side-effect cleanup, and reset timers. + const callbackRefs = useRef( { onDismiss, onRemove } ); + useLayoutEffect( () => { + callbackRefs.current = { onDismiss, onRemove }; + } ); + useEffect( () => { + // Only set up the timeout dismiss if we're not explicitly dismissing. const timeoutHandle = setTimeout( () => { if ( ! explicitDismiss ) { - onDismiss?.(); - onRemove?.(); + callbackRefs.current.onDismiss?.(); + callbackRefs.current.onRemove?.(); } }, NOTICE_TIMEOUT ); return () => clearTimeout( timeoutHandle ); - }, [ onDismiss, onRemove, explicitDismiss ] ); + }, [ explicitDismiss ] ); const classes = classnames( className, 'components-snackbar', { 'components-snackbar-explicit-dismiss': !! explicitDismiss, @@ -106,7 +119,7 @@ function UnforwardedSnackbar( if ( actions && actions.length > 1 ) { // We need to inform developers that snackbar only accepts 1 action. warning( - 'Snackbar can only have 1 action, use Notice if your message require many messages' + 'Snackbar can only have one action. Use Notice if your message requires many actions.' ); // return first element only while keeping it inside an array actions = [ actions[ 0 ] ]; diff --git a/packages/components/src/tabs/index.tsx b/packages/components/src/tabs/index.tsx index 7f738cb9f08a96..4573f7a6968df9 100644 --- a/packages/components/src/tabs/index.tsx +++ b/packages/components/src/tabs/index.tsx @@ -8,7 +8,12 @@ import * as Ariakit from '@ariakit/react'; * WordPress dependencies */ import { useInstanceId } from '@wordpress/compose'; -import { useLayoutEffect, useMemo, useRef } from '@wordpress/element'; +import { + useEffect, + useLayoutEffect, + useMemo, + useRef, +} from '@wordpress/element'; /** * Internal dependencies @@ -44,8 +49,8 @@ function Tabs( { const isControlled = selectedTabId !== undefined; - const { items, selectedId } = store.useState(); - const { setSelectedId, move } = store; + const { items, selectedId, activeId } = store.useState(); + const { setSelectedId, setActiveId } = store; // Keep track of whether tabs have been populated. This is used to prevent // certain effects from firing too early while tab data and relevant @@ -154,26 +159,31 @@ function Tabs( { setSelectedId, ] ); - // In controlled mode, make sure browser focus follows the selected tab if - // the selection is changed while a tab is already being focused. - useLayoutEffect( () => { - if ( ! isControlled || ! selectOnMove ) { + useEffect( () => { + if ( ! isControlled ) { return; } - const currentItem = items.find( ( item ) => item.id === selectedId ); - const activeElement = currentItem?.element?.ownerDocument.activeElement; - const tabsHasFocus = items.some( ( item ) => { - return activeElement && activeElement === item.element; - } ); - if ( - activeElement && - tabsHasFocus && - selectedId !== activeElement.id - ) { - move( selectedId ); - } - }, [ isControlled, items, move, selectOnMove, selectedId ] ); + requestAnimationFrame( () => { + const focusedElement = + items?.[ 0 ]?.element?.ownerDocument.activeElement; + + if ( + ! focusedElement || + ! items.some( ( item ) => focusedElement === item.element ) + ) { + return; // Return early if no tabs are focused. + } + + // If, after ariakit re-computes the active tab, that tab doesn't match + // the currently focused tab, then we force an update to ariakit to avoid + // any mismatches, especially when navigating to previous/next tab with + // arrow keys. + if ( activeId !== focusedElement.id ) { + setActiveId( focusedElement.id ); + } + } ); + }, [ activeId, isControlled, items, setActiveId ] ); const contextValue = useMemo( () => ( { diff --git a/packages/components/src/tabs/tablist.tsx b/packages/components/src/tabs/tablist.tsx index 7a53115910796c..afa2d8283e6d6c 100644 --- a/packages/components/src/tabs/tablist.tsx +++ b/packages/components/src/tabs/tablist.tsx @@ -28,11 +28,30 @@ export const TabList = forwardRef< return null; } const { store } = context; + + const { selectedId, activeId, selectOnMove } = store.useState(); + const { setActiveId } = store; + + const onBlur = () => { + if ( ! selectOnMove ) { + return; + } + + // When automatic tab selection is on, make sure that the active tab is up + // to date with the selected tab when leaving the tablist. This makes sure + // that the selected tab will receive keyboard focus when tabbing back into + // the tablist. + if ( selectedId !== activeId ) { + setActiveId( selectedId ); + } + }; + return ( } + onBlur={ onBlur } { ...otherProps } > { children } diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index 8c2c2d0fd2fa2a..ca9a01928e599c 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -1172,6 +1172,111 @@ describe( 'Tabs', () => { ).not.toBeInTheDocument(); } ); } ); + describe( 'When `selectedId` is changed by the controlling component', () => { + describe.each( [ true, false ] )( + 'and `selectOnMove` is %s', + ( selectOnMove ) => { + it( 'should continue to handle arrow key navigation properly', async () => { + const { rerender } = render( + + ); + + // Tab key should focus the currently selected tab, which is Beta. + await press.Tab(); + expect( await getSelectedTab() ).toHaveTextContent( + 'Beta' + ); + expect( await getSelectedTab() ).toHaveFocus(); + + rerender( + + ); + + // When the selected tab is changed, it should not automatically receive focus. + expect( await getSelectedTab() ).toHaveTextContent( + 'Gamma' + ); + expect( + screen.getByRole( 'tab', { name: 'Beta' } ) + ).toHaveFocus(); + + // Arrow keys should move focus to the next tab, which is Gamma + await press.ArrowRight(); + expect( + screen.getByRole( 'tab', { name: 'Gamma' } ) + ).toHaveFocus(); + } ); + + it( 'should focus the correct tab when tabbing out and back into the tablist', async () => { + const { rerender } = render( + <> + + + + ); + + // Tab key should focus the currently selected tab, which is Beta. + await press.Tab(); + await press.Tab(); + expect( await getSelectedTab() ).toHaveTextContent( + 'Beta' + ); + expect( await getSelectedTab() ).toHaveFocus(); + + rerender( + <> + + + + ); + + // When the selected tab is changed, it should not automatically receive focus. + expect( await getSelectedTab() ).toHaveTextContent( + 'Gamma' + ); + expect( + screen.getByRole( 'tab', { name: 'Beta' } ) + ).toHaveFocus(); + + // Press shift+tab, move focus to the button before Tabs + await press.ShiftTab(); + expect( + screen.getByRole( 'button', { name: 'Focus me' } ) + ).toHaveFocus(); + + // Press tab, move focus back to the tablist + await press.Tab(); + + const betaTab = screen.getByRole( 'tab', { + name: 'Beta', + } ); + const gammaTab = screen.getByRole( 'tab', { + name: 'Gamma', + } ); + + expect( + selectOnMove ? gammaTab : betaTab + ).toHaveFocus(); + } ); + } + ); + } ); describe( 'When `selectOnMove` is `true`', () => { it( 'should automatically select a newly focused tab', async () => { @@ -1188,24 +1293,6 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); } ); - it( 'should automatically update focus when the selected tab is changed by the controlling component', async () => { - const { rerender } = render( - - ); - - // Tab key should focus the currently selected tab, which is Beta. - await press.Tab(); - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - expect( await getSelectedTab() ).toHaveFocus(); - - rerender( - - ); - - // When the selected tab is changed, it should automatically receive focus. - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( await getSelectedTab() ).toHaveFocus(); - } ); } ); describe( 'When `selectOnMove` is `false`', () => { it( 'should apply focus without automatically changing the selected tab', async () => { @@ -1247,37 +1334,6 @@ describe( 'Tabs', () => { await press.Enter(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); } ); - it( 'should not automatically update focus when the selected tab is changed by the controlling component', async () => { - const { rerender } = render( - - ); - - expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); - - // Tab key should focus the currently selected tab, which is Beta. - await press.Tab(); - await waitFor( async () => - expect( await getSelectedTab() ).toHaveFocus() - ); - - rerender( - - ); - - // When the selected tab is changed, it should not automatically receive focus. - expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - expect( - screen.getByRole( 'tab', { name: 'Beta' } ) - ).toHaveFocus(); - } ); } ); } ); it( 'should associate each `Tab` with the correct `TabPanel`, even if they are not rendered in the same order', async () => { diff --git a/packages/e2e-test-utils-playwright/src/request-utils/users.ts b/packages/e2e-test-utils-playwright/src/request-utils/users.ts index 0997a7f51b5cb0..11df5f37468ca6 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/users.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/users.ts @@ -5,6 +5,7 @@ import type { RequestUtils } from './index'; export interface User { id: number; + name: string; email: string; } @@ -110,8 +111,11 @@ async function deleteAllUsers( this: RequestUtils ) { // The users endpoint doesn't support batch request yet. const responses = await Promise.all( users - // Do not delete root user. - .filter( ( user: User ) => user.id !== 1 ) + // Do not delete neither root user nor the current user. + .filter( + ( user: User ) => + user.id !== 1 && user.name !== this.user.username + ) .map( ( user: User ) => deleteUser.bind( this )( user.id ) ) ); diff --git a/packages/e2e-tests/plugins/block-bindings.php b/packages/e2e-tests/plugins/block-bindings.php new file mode 100644 index 00000000000000..d61f00f9c43872 --- /dev/null +++ b/packages/e2e-tests/plugins/block-bindings.php @@ -0,0 +1,36 @@ + true, + 'type' => 'string', + 'single' => true, + 'default' => 'Value of the text_custom_field', + ) + ); + // TODO: Change url. + register_meta( + 'post', + 'url_custom_field', + array( + 'show_in_rest' => true, + 'type' => 'string', + 'single' => true, + 'default' => '', + ) + ); +} +add_action( 'init', 'gutenberg_test_block_bindings_register_custom_fields' ); diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js index 4f7efaa5948998..244e21b1acd432 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -4,6 +4,7 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; +import { forwardRef } from '@wordpress/element'; import { store as editorStore } from '@wordpress/editor'; /** @@ -14,7 +15,7 @@ import { sidebars } from '../settings-sidebar'; const { Tabs } = unlock( componentsPrivateApis ); -const SettingsHeader = () => { +const SettingsHeader = ( _, ref ) => { const { documentLabel } = useSelect( ( select ) => { const { getPostTypeLabel } = select( editorStore ); @@ -25,9 +26,19 @@ const SettingsHeader = () => { }, [] ); return ( - - { documentLabel } - + + + { documentLabel } + + { /* translators: Text label for the Block Settings Sidebar tab. */ } { __( 'Block' ) } @@ -35,4 +46,4 @@ const SettingsHeader = () => { ); }; -export default SettingsHeader; +export default forwardRef( SettingsHeader ); diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 44900fff3f7be2..31381fad5a2c44 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -6,7 +6,13 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; -import { Platform, useCallback, useContext } from '@wordpress/element'; +import { + Platform, + useCallback, + useContext, + useEffect, + useRef, +} from '@wordpress/element'; import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as interfaceStore } from '@wordpress/interface'; @@ -50,17 +56,45 @@ const SidebarContent = ( { keyboardShortcut, isEditingTemplate, } ) => { + const tabListRef = useRef( null ); // Because `PluginSidebarEditPost` renders a `ComplementaryArea`, we // need to forward the `Tabs` context so it can be passed through the // underlying slot/fill. const tabsContextValue = useContext( Tabs.Context ); + // This effect addresses a race condition caused by tabbing from the last + // block in the editor into the settings sidebar. Without this effect, the + // selected tab and browser focus can become separated in an unexpected way + // (e.g the "block" tab is focused, but the "post" tab is selected). + useEffect( () => { + const tabsElements = Array.from( + tabListRef.current?.querySelectorAll( '[role="tab"]' ) || [] + ); + const selectedTabElement = tabsElements.find( + // We are purposefully using a custom `data-tab-id` attribute here + // because we don't want rely on any assumptions about `Tabs` + // component internals. + ( element ) => element.getAttribute( 'data-tab-id' ) === sidebarName + ); + const activeElement = selectedTabElement?.ownerDocument.activeElement; + const tabsHasFocus = tabsElements.some( ( element ) => { + return activeElement && activeElement.id === element.id; + } ); + if ( + tabsHasFocus && + selectedTabElement && + selectedTabElement.id !== activeElement?.id + ) { + selectedTabElement?.focus(); + } + }, [ sidebarName ] ); + return ( - + } closeLabel={ __( 'Close Settings' ) } @@ -157,6 +191,7 @@ const SettingsSidebar = () => { // the selected tab to `null` avoids that. selectedTabId={ isSettingsSidebarActive ? sidebarName : null } onSelect={ onTabSelect } + selectOnMove={ false } > toggleModal() } - initialTabName={ modalTabOpen } + initialTabId={ modalTabOpen } /> ) } @@ -47,7 +47,7 @@ function FontFamilies() { toggleModal( 'installed-fonts' ) } aria-label={ __( 'Manage fonts' ) } - icon={ typography } + icon={ settings } size={ 'small' } /> @@ -63,7 +63,16 @@ function FontFamilies() { ) ) } ) : ( - <>{ __( 'No fonts installed.' ) } + <> + { __( 'No fonts installed.' ) } + + ) } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js index ab090a0cd0b4db..0db8932a98e34d 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js @@ -35,21 +35,23 @@ function CollectionFontVariant( { ); return ( -
  • diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js b/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js index cc62a85193669b..f58eccd2b6ec2d 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js @@ -45,21 +45,23 @@ function LibraryFontVariant( { face, font } ) { ); return ( -