diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index d1621ed5106aa..149faee274206 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -69,7 +69,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: token: ${{ secrets.GUTENBERG_TOKEN }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -165,7 +165,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: ref: ${{ needs.bump-version.outputs.release_branch || github.ref }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -222,7 +222,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: fetch-depth: 2 ref: ${{ needs.bump-version.outputs.release_branch }} @@ -311,14 +311,14 @@ jobs: if: ${{ endsWith( needs.bump-version.outputs.new_version, '-rc.1' ) }} steps: - name: Checkout (for CLI) - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: path: main ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Checkout (for publishing) - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: path: publish # Later, we switch this branch in the script that publishes packages. diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 8eafe4267bc43..1065421044373 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: fetch-depth: 1 show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/check-components-changelog.yml b/.github/workflows/check-components-changelog.yml index 77fdf4759f7de..d995d641fae57 100644 --- a/.github/workflows/check-components-changelog.yml +++ b/.github/workflows/check-components-changelog.yml @@ -20,7 +20,7 @@ jobs: - name: 'Get PR commit count' run: echo "PR_COMMIT_COUNT=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}" - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml index 2b93546926480..7c26cb6e14e76 100644 --- a/.github/workflows/create-block.yml +++ b/.github/workflows/create-block.yml @@ -24,7 +24,7 @@ jobs: os: ['macos-latest', 'ubuntu-latest', 'windows-latest'] steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index c919e733360ff..16680038e0db6 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -27,7 +27,7 @@ jobs: totalParts: [8] steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -53,35 +53,61 @@ jobs: - name: Archive debug artifacts (screenshots, traces) uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 - if: always() + if: ${{ !cancelled() }} with: - name: failures-artifacts + name: failures-artifacts--${{ matrix.part }} path: artifacts/test-results if-no-files-found: ignore - name: Archive flaky tests report uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 - if: always() + if: ${{ !cancelled() }} with: - name: flaky-tests-report + name: flaky-tests-report--${{ matrix.part }} path: flaky-tests if-no-files-found: ignore + merge-artifacts: + name: Merge Artifacts + if: ${{ !cancelled() }} + needs: [e2e-playwright] + runs-on: ubuntu-latest + outputs: + has-flaky-test-report: ${{ !!steps.merge-flaky-tests-reports.outputs.artifact-id }} + steps: + - name: Merge failures artifacts + uses: actions/upload-artifact/merge@v4 + # Don't fail the job if there aren't any artifacts to merge. + continue-on-error: true + with: + name: failures-artifacts + # Retain the merged artifacts in case of a rerun. + pattern: failures-artifacts* + delete-merged: true + + - name: Merge flaky tests reports + id: merge-flaky-tests-reports + uses: actions/upload-artifact/merge@v4 + continue-on-error: true + with: + name: flaky-tests-report + pattern: flaky-tests-report* + delete-merged: true + report-to-issues: name: Report to GitHub - needs: [e2e-playwright] - if: ${{ always() }} + needs: [merge-artifacts] + if: ${{ needs.merge-artifacts.outputs.has-flaky-test-report == 'true' }} runs-on: ubuntu-latest steps: # Checkout defaults to using the branch which triggered the event, which # isn't necessarily `trunk` (e.g. in the case of a merge). - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - uses: actions/download-artifact@v4.1.7 - id: download_artifact # Don't fail the job if there isn't any flaky tests report. continue-on-error: true with: @@ -89,16 +115,13 @@ jobs: path: flaky-tests - name: Setup Node.js and install dependencies - if: ${{ steps.download_artifact.outcome == 'success' }} uses: ./.github/setup-node - name: Npm build - if: ${{ steps.download_artifact.outcome == 'success' }} # TODO: We don't have to build the entire project, just the action itself. run: npm run build:packages - name: Report flaky tests - if: ${{ steps.download_artifact.outcome == 'success' }} uses: ./packages/report-flaky-tests with: repo-token: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index bcd0fee6453fd..633f62d5ed28c 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -6,7 +6,7 @@ jobs: name: 'Validation' runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - uses: gradle/wrapper-validation-action@v3 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index b78ff9532c22d..7f239652774df 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -32,7 +32,7 @@ jobs: WP_ARTIFACTS_PATH: ${{ github.workspace }}/artifacts steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -84,6 +84,9 @@ jobs: run: | ./bin/plugin/cli.js perf $(echo $BRANCHES | tr ',' ' ') --tests-branch $GITHUB_SHA --wp-version "$WP_VERSION" + - name: Add workflow summary + run: cat ${{ env.WP_ARTIFACTS_PATH }}/summary.md >> $GITHUB_STEP_SUMMARY + - name: Archive performance results if: success() uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 diff --git a/.github/workflows/php-changes-detection.yml b/.github/workflows/php-changes-detection.yml index ba34e0d806185..5a7749ee21381 100644 --- a/.github/workflows/php-changes-detection.yml +++ b/.github/workflows/php-changes-detection.yml @@ -10,7 +10,7 @@ jobs: if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} steps: - name: Check out code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: fetch-depth: 0 show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -20,7 +20,9 @@ jobs: uses: tj-actions/changed-files@0874344d6ebbaa00a27da73276ae7162fadcaf69 # v44.3.0 with: files: | - **.{php} + lib/** + packages/**/*.php + phpunit/** - name: List all changed files if: steps.changed-files-php.outputs.any_changed == 'true' diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml index 11dfdb878ef28..94397afd7b4bc 100644 --- a/.github/workflows/publish-npm-packages.yml +++ b/.github/workflows/publish-npm-packages.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout (for CLI) if: ${{ github.event.inputs.release_type != 'wp' }} - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: path: cli ref: trunk @@ -39,7 +39,7 @@ jobs: - name: Checkout (for publishing) if: ${{ github.event.inputs.release_type != 'wp' }} - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: path: publish # Later, we switch this branch in the script that publishes packages. @@ -49,7 +49,7 @@ jobs: - name: Checkout (for publishing WP major version) if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: path: publish ref: wp/${{ github.event.inputs.wp_version }} diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml index 05d28f888d0ae..099203bbffe72 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -12,7 +12,7 @@ jobs: steps: # Checkout defaults to using the branch which triggered the event, which # isn't necessarily `trunk` (e.g. in the case of a merge). - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index 5d1d476226b12..a4dce407d1c0f 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -23,7 +23,7 @@ jobs: steps: - name: checkout - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index 5056527d097bd..516f783c11e40 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -23,7 +23,7 @@ jobs: native-test-name: [gutenberg-editor-rendering] steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index 12c8931efca06..ff3fe96d505f6 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -22,7 +22,7 @@ jobs: if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/storybook-pages.yml b/.github/workflows/storybook-pages.yml index 56b7471f06d9b..7486ea32533e6 100644 --- a/.github/workflows/storybook-pages.yml +++ b/.github/workflows/storybook-pages.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 22bca2dc78186..a4a639e183d5b 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -70,7 +70,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -121,7 +121,7 @@ jobs: name: Build JavaScript assets for PHP unit tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -170,7 +170,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -281,7 +281,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} @@ -351,7 +351,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index 8a92d0443d577..8f57a749b0601 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -96,7 +96,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: ref: ${{ matrix.branch }} token: ${{ secrets.GUTENBERG_TOKEN }} diff --git a/README.md b/README.md index 360b2851be092..9c920337ef594 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Get hands on: check out the [block editor live demo](https://wordpress.org/guten Extending and customizing is at the heart of the WordPress platform, this is no different for the Gutenberg project. The editor and future products can be extended by third-party developers using plugins. -Review the [Create a Block tutorial](/docs/getting-started/devenv/get-started-with-create-block.md) for the fastest way to get started extending the block editor. See the [Developer Documentation](https://developer.wordpress.org/block-editor/#develop-for-the-block-editor) for extensive tutorials, documentation, and API references. +Review the [Quick Start Guide](https://developer.wordpress.org/block-editor/getting-started/quick-start-guide/) for the fastest way to get started extending the block editor. See the [Block Editor Handbook](https://developer.wordpress.org/block-editor/) for extensive tutorials, documentation, and API references. Also, check the [WordPress Developer Blog](https://developer.wordpress.org/blog/) for great articles about block development, among other topics. ### Contribute to Gutenberg diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js index 9d9b39fce0984..65b8a770d3764 100644 --- a/bin/plugin/commands/performance.js +++ b/bin/plugin/commands/performance.js @@ -96,6 +96,79 @@ async function runTestSuite( testSuite, testRunnerDir, runKey ) { ); } +/** + * Formats an array of objects as a Markdown table. + * + * For example, this array: + * + * [ + * { + * foo: 123, + * bar: 456, + * baz: 'Yes', + * }, + * { + * foo: 777, + * bar: 999, + * baz: 'No', + * } + * ] + * + * Will result in the following table: + * + * | foo | bar | baz | + * |-----|-----|-----| + * | 123 | 456 | Yes | + * | 777 | 999 | No | + * + * @param {Array} rows Table rows. + * @return {string} Markdown table content. + */ +function formatAsMarkdownTable( rows ) { + let result = ''; + + if ( ! rows.length ) { + return result; + } + + const headers = Object.keys( rows[ 0 ] ); + for ( const header of headers ) { + result += `| ${ header } `; + } + result += '|\n'; + for ( let i = 0; i < headers.length; i++ ) { + result += '| ------ '; + } + result += '|\n'; + + for ( const row of rows ) { + for ( const value of Object.values( row ) ) { + result += `| ${ value } `; + } + result += '|\n'; + } + + return result; +} + +/** + * Nicely formats a given value. + * + * @param {string} metric Metric. + * @param {number} value + */ +function formatValue( metric, value ) { + if ( 'wpMemoryUsage' === metric ) { + return `${ ( value / Math.pow( 10, 6 ) ).toFixed( 2 ) } MB`; + } + + if ( 'wpDbQueries' === metric ) { + return value.toString(); + } + + return `${ value } ms`; +} + /** * Runs the performances tests on an array of branches and output the result. * @@ -387,7 +460,7 @@ async function runPerformanceTests( branches, options ) { return readJSONFile( file ); } ); - const metrics = Object.keys( resultsRounds[ 0 ] ); + const metrics = Object.keys( resultsRounds[ 0 ] ?? {} ); results[ testSuite ][ branch ] = {}; for ( const metric of metrics ) { @@ -401,6 +474,7 @@ async function runPerformanceTests( branches, options ) { } } } + const calculatedResultsPath = path.join( ARTIFACTS_PATH, testSuite + RESULTS_FILE_SUFFIX @@ -424,6 +498,10 @@ async function runPerformanceTests( branches, options ) { ) ); + let summaryMarkdown = `## Performance Test Results\n\n`; + + summaryMarkdown += `Please note that client side metrics **exclude** the server response time.\n\n`; + for ( const testSuite of testSuites ) { logAtIndent( 0, formats.success( testSuite ) ); @@ -435,7 +513,10 @@ async function runPerformanceTests( branches, options ) { ) ) { for ( const [ metric, value ] of Object.entries( metrics ) ) { invertedResult[ metric ] = invertedResult[ metric ] || {}; - invertedResult[ metric ][ branch ] = `${ value } ms`; + invertedResult[ metric ][ branch ] = formatValue( + metric, + value + ); } } @@ -457,7 +538,37 @@ async function runPerformanceTests( branches, options ) { // Print the results. console.table( invertedResult ); + + // Use yet another structure to generate a Markdown table. + + const rows = []; + + for ( const [ metric, resultBranches ] of Object.entries( + invertedResult + ) ) { + /** + * @type {Record< string, string >} + */ + const row = { + Metric: metric, + }; + + for ( const [ branch, value ] of Object.entries( + resultBranches + ) ) { + row[ branch ] = value; + } + rows.push( row ); + } + + summaryMarkdown += `**${ testSuite }**\n\n`; + summaryMarkdown += `${ formatAsMarkdownTable( rows ) }\n`; } + + fs.writeFileSync( + path.join( ARTIFACTS_PATH, 'summary.md' ), + summaryMarkdown + ); } module.exports = { diff --git a/bin/test-create-block.sh b/bin/test-create-block.sh index 7959334a8e30e..99b7e8e608260 100755 --- a/bin/test-create-block.sh +++ b/bin/test-create-block.sh @@ -69,7 +69,7 @@ status "Building block..." ../node_modules/.bin/wp-scripts build status "Verifying build..." -expected=7 +expected=9 actual=$( find build -maxdepth 1 -type f | wc -l ) if [ "$expected" -ne "$actual" ]; then error "Expected $expected files in the \`build\` directory, but found $actual." diff --git a/changelog.txt b/changelog.txt index aee9a5c1a1575..0bb052d82feb0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,6 @@ == Changelog == -= 18.3.0-rc.1 = - += 18.3.0 = ## Changelog @@ -233,6 +232,8 @@ The following contributors merged PRs in this release: @aaronrobertshaw @afercia @ajlende @carolinan @cbravobernal @colorful-tones @DaniGuardiola @desrosj @draganescu @ellatrix @fabiankaegy @fullofcaffeine @geriux @huubl @itzmekhokan @jameskoster @jasmussen @jeryj @jorgefilipecosta @jsnajdr @juanfra @juanmaguitar @lanresmith @MaggieCabrera @Mamaduka @mirka @ntsekouras @oandregal @ockham @ramonjd @retrofox @richtabor @SantosGuillamot @scruffian @shail-mehta @sirreal @stokesman @sunil25393 @swissspidy @t-hamano @talldan @twstokes @tyxla @youknowriad + + = 18.2.0 = ## Changelog diff --git a/docs/contributors/code/e2e/README.md b/docs/contributors/code/e2e/README.md index 43443cddd6aeb..3a123cc2988b7 100644 --- a/docs/contributors/code/e2e/README.md +++ b/docs/contributors/code/e2e/README.md @@ -36,6 +36,7 @@ xvfb-run npm run test:e2e # Only run webkit tests. xvfb-run -- npm run test:e2e -- --project=webkit ``` +If you're already editing in VS Code, you may find the [Playwright extension](https://playwright.dev/docs/getting-started-vscode) helpful for running, writing and debugging tests. ## Best practices diff --git a/docs/contributors/code/getting-started-with-code-contribution.md b/docs/contributors/code/getting-started-with-code-contribution.md index 30a78037ab75e..921c8ad6ddc3e 100644 --- a/docs/contributors/code/getting-started-with-code-contribution.md +++ b/docs/contributors/code/getting-started-with-code-contribution.md @@ -126,7 +126,7 @@ Port: {MYSQL_PORT_NUMBER} **Please note**: the MySQL port number will change each time `wp-env` restarts. If you find you can no longer access your database, simply repeat the steps above to find the new port number and restore your connection. -**Tip**: [Sequel Ace](https://sequel-ace.com/) is a useful GUI tool for accessing a MySQL database. Other tools are available and documented in this [article on accessing the WordPress database](https://wordpress.org/documentation/article/creating-database-for-wordpress/). +**Tip**: [Sequel Ace](https://sequel-ace.com/) is a useful GUI tool for accessing a MySQL database. Other tools are available and documented in this [article on accessing the WordPress database](https://developer.wordpress.org/advanced-administration/before-install/creating-database/). #### Troubleshooting diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index 57a379a9fafe1..72d7bfd760d65 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -68,219 +68,6 @@ Yes. There are a lot! There is a help modal showing all available keyboard short You can see the whole list going to the top right corner menu of the new editor and clicking on “Keyboard Shortcuts” (or by using the keyboard shortcut Shift+Alt+H on Linux/Windows and H on macOS). -This is the canonical list of keyboard shortcuts: - -#### Editor shortcuts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Shortcut descriptionLinux/Windows shortcutmacOS shortcut
Display keyboard shortcuts.Shift+Alt+HH
Save your changes.Ctrl+SS
Undo your last changes.Ctrl+ZZ
Redo your last undo.Ctrl+Shift+ZZ
Show or hide the Settings sidebar.Ctrl+Shift+,,
Open the list view menu.Shift+Alt+OO
Navigate to the next part of the editor.Ctrl+``
Navigate to the previous part of the editor.Ctrl+Shift+``
Navigate to the next part of the editor (alternative).Ctrl+Alt+NN
Navigate to the previous part of the editor (alternative).Ctrl+Alt+PP
Navigate to the nearest toolbar.Alt+F10F10
Switch between visual editor and code editor.Ctrl+Shift+Alt+MM
Toggle fullscreen mode.Ctrl+Alt+Shift+FF
- -#### Selection shortcuts - - - - - - - - - - - - - - - - - - - - - - - - - - -
Shortcut descriptionLinux/Windows shortcutmacOS shortcut
Select all text when typing. Press again to select all blocks.Ctrl+AA
Clear selection.EscEsc
Select text across multiple blocks.Shift+Arrow (⇦, ⇧, ⇨, ⇩)Shift+Arrow (⇦, ⇧, ⇨, ⇩)
- -#### Block shortcuts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Shortcut descriptionLinux/Windows shortcutmacOS shortcut
Duplicate the selected block(s).Ctrl+Shift+DD
Remove the selected block(s).Shift+Alt+ZZ
Insert a new block before the selected block(s).Ctrl+Alt+TT
Insert a new block after the selected block(s).Ctrl+Alt+YY
Move the selected block(s) up.Ctrl+Alt+Shift+TT
Move the selected block(s) down.Ctrl+Alt+Shift+YY
Change the block type after adding a new paragraph.//
Remove multiple selected blocks.delbackspacedelbackspace
- -#### Text formatting - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Shortcut descriptionLinux/Windows shortcutmacOS shortcut
Make the selected text bold.Ctrl+BB
Make the selected text italic.Ctrl+II
Underline the selected text.Ctrl+UU
Convert the selected text into a link.Ctrl+KK
Remove a link.Ctrl+Shift+KK
Add a strikethrough to the selected text.Shift+Alt+DD
Display the selected text in a monospaced font.Shift+Alt+XX
- Here is a brief animation illustrating how to find and use the keyboard shortcuts: ![GIF showing how to access keyboard shortcuts](https://make.wordpress.org/core/files/2020/07/keyboard-shortcuts.gif) diff --git a/docs/how-to-guides/themes/global-settings-and-styles.md b/docs/how-to-guides/themes/global-settings-and-styles.md index 730595df4d4be..69f0606c93649 100644 --- a/docs/how-to-guides/themes/global-settings-and-styles.md +++ b/docs/how-to-guides/themes/global-settings-and-styles.md @@ -1337,4 +1337,4 @@ The value defined for the root `styles.spacing.blockGap` style is also output as ### Why does it take so long to update the styles in the browser? -When you are actively developing with theme.json you may notice it takes 30+ seconds for your changes to show up in the browser, this is because `theme.json` is cached. To remove this caching issue, set either [`WP_DEBUG`](https://wordpress.org/documentation/article/debugging-in-wordpress/#wp_debug) or [`SCRIPT_DEBUG`](https://wordpress.org/documentation/article/debugging-in-wordpress/#script_debug) to 'true' in your [`wp-config.php`](https://wordpress.org/documentation/article/editing-wp-config-php/). This tells WordPress to skip the cache and always use fresh data. +When you are actively developing with theme.json you may notice it takes 30+ seconds for your changes to show up in the browser, this is because `theme.json` is cached. To remove this caching issue, set either [`WP_DEBUG`](https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/#wp_debug) or [`SCRIPT_DEBUG`](https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/#script_debug) to 'true' in your [`wp-config.php`](https://developer.wordpress.org/advanced-administration/wordpress/wp-config/). This tells WordPress to skip the cache and always use fresh data. diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 3a0a88048e982..c08869db34b48 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -415,7 +415,7 @@ Create a bulleted or numbered list. ([Source](https://github.com/WordPress/guten - **Name:** core/list - **Category:** text - **Allowed Blocks:** core/list-item -- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~className~~, ~~html~~ +- **Supports:** __unstablePasteTextInline, anchor, color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** ordered, placeholder, reversed, start, type, values ## List item diff --git a/docs/reference-guides/interactivity-api/README.md b/docs/reference-guides/interactivity-api/README.md index 1401c7f9bfecc..b6e0d639c3fc8 100644 --- a/docs/reference-guides/interactivity-api/README.md +++ b/docs/reference-guides/interactivity-api/README.md @@ -67,6 +67,19 @@ The Interactivity API provides the `@wordpress/interactivity` Script Module. Jav } ``` +The use of `viewScriptModule` also requires the `--experimental-modules` flag for both the [`build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build) and [`start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start) scripts of `wp-scripts` to ensure a proper build of the Script Modules. + + +```json +// package.json +{ + "scripts": { + ... + "build": "wp-scripts build --experimental-modules", + "start": "wp-scripts start --experimental-modules" + } +``` + #### Add `wp-interactive` directive to a DOM element To "activate" the Interactivity API in a DOM element (and its children), add the [`wp-interactive`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-interactivity/packages-interactivity-api-reference/#wp-interactive) directive to the DOM element in the block's `render.php` or `save.js` files. diff --git a/docs/reference-guides/interactivity-api/iapi-about.md b/docs/reference-guides/interactivity-api/iapi-about.md index f09ef77e6211b..887a284be298f 100644 --- a/docs/reference-guides/interactivity-api/iapi-about.md +++ b/docs/reference-guides/interactivity-api/iapi-about.md @@ -50,13 +50,13 @@ _Dynamic block example_ Toggle -

+

This element is now visible!

``` -As you can see, directives like `data-wp-on--click` or `data-wp-show` are added as custom HTML attributes. WordPress can process this HTML on the server, handling the directives’ logic and creating the appropriate markup. +As you can see, directives like [`data-wp-on--click`](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#wp-on) or [`data-wp-bind--hidden`](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#wp-bind) are added as custom HTML attributes. WordPress can process this HTML on the server, handling the directives’ logic and creating the appropriate markup. ### Backward compatible @@ -136,7 +136,7 @@ store( 'wpmovies', { Toggle -

+

This element is now visible!

@@ -155,15 +155,13 @@ The API has been designed to be as performant as possible: Directives can be added, removed, or modified directly from the HTML. For example, users could use the [`render_block` filter](https://developer.wordpress.org/reference/hooks/render_block/) to modify the HTML and its behavior. -In addition to using built-in directives, users can create custom directives to add any custom behaviors to their HTML. - ### Atomic and composable Each directive controls a small part of the DOM, and you can combine multiple directives to create rich, interactive user experiences. ### Compatible with the existing block development tooling -Using built-in directives does not require a build step and only requires a small runtime. A build step is necessary only when creating custom directives that return JSX. For such use cases, the API works out of the box with common block-building tools like [`wp-scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/). +The API works out of the box with standard block-building tools like [`wp-scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/). The only requirement for `wp-scripts` to properly build the [Script Modules](https://make.wordpress.org/core/2024/03/04/script-modules-in-6-5/) using the Interactivity API is the use of the --experimental-modules flag for both [`build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build) and [`start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start) scripts. ### Client-side navigation diff --git a/docs/reference-guides/interactivity-api/iapi-faq.md b/docs/reference-guides/interactivity-api/iapi-faq.md index 2e0e0ce4da430..fba3603f39169 100644 --- a/docs/reference-guides/interactivity-api/iapi-faq.md +++ b/docs/reference-guides/interactivity-api/iapi-faq.md @@ -99,7 +99,7 @@ The API has been designed with performance in mind, so it shouldn’t be a probl ## Does it work with the Core Translation API? -As the Interactivity API works perfectly with server-side rendering, you can use all the WordPress APIs including [`__()`](https://developer.wordpress.org/reference/functions/__/) and [`_e()`](https://developer.wordpress.org/reference/functions/_e/). You can use it to translate the text in the HTML (as you normally would) and even use it inside the store when using `wp_initial_state()` on the server side. It might look something like this: +As the Interactivity API works perfectly with server-side rendering, you can use all the WordPress APIs including [`__()`](https://developer.wordpress.org/reference/functions/__/) and [`_e()`](https://developer.wordpress.org/reference/functions/_e/). You can use it to translate the text in the HTML (as you normally would) and even use it inside the store when [using `wp_interactivity_state()` on the server side](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/api-reference/#setting-the-store). It might look something like this: ```php // render.php @@ -111,8 +111,7 @@ wp_interactivity_state( 'favoriteMovies', array( ) ); ``` -A translation API compatible with script modules (needed for the Interactivity API) is -currently being worked on. Check [#60234](https://core.trac.wordpress.org/ticket/60234) to follow the progress on this work. +A translation API compatible with script modules (needed for the Interactivity API) is currently being worked on. Check [#60234](https://core.trac.wordpress.org/ticket/60234) to follow the progress on this work. ## I’m concerned about XSS; can JavaScript be injected into directives? diff --git a/lib/blocks.php b/lib/blocks.php index eed9142db436a..679219cc6ff77 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -26,7 +26,6 @@ function gutenberg_reregister_core_block_types() { 'form-submit-button', 'group', 'html', - 'list', 'list-item', 'missing', 'more', @@ -76,6 +75,7 @@ function gutenberg_reregister_core_block_types() { 'heading.php' => 'core/heading', 'latest-comments.php' => 'core/latest-comments', 'latest-posts.php' => 'core/latest-posts', + 'list.php' => 'core/list', 'loginout.php' => 'core/loginout', 'media-text.php' => 'core/media-text', 'navigation.php' => 'core/navigation', diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 46d7082912f53..1dec7b164d880 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -320,7 +320,6 @@ class WP_Theme_JSON_Gutenberg { ), 'background-image' => array( array( 'background', 'backgroundImage', 'url' ), - array( 'background', 'backgroundImage', 'source' ), ), ); diff --git a/lib/compat/wordpress-6.5/blocks.php b/lib/compat/wordpress-6.5/blocks.php index a695d075c0b52..89bb22825103a 100644 --- a/lib/compat/wordpress-6.5/blocks.php +++ b/lib/compat/wordpress-6.5/blocks.php @@ -65,68 +65,27 @@ function gutenberg_block_bindings_replace_html( $block_content, $block_name, str switch ( $block_type->attributes[ $attribute_name ]['source'] ) { case 'html': case 'rich-text': - $block_reader = new WP_HTML_Tag_Processor( $block_content ); - - // TODO: Support for CSS selectors whenever they are ready in the HTML API. - // In the meantime, support comma-separated selectors by exploding them into an array. - $selectors = explode( ',', $block_type->attributes[ $attribute_name ]['selector'] ); - // Add a bookmark to the first tag to be able to iterate over the selectors. - $block_reader->next_tag(); - $block_reader->set_bookmark( 'iterate-selectors' ); - - // TODO: This shouldn't be needed when the `set_inner_html` function is ready. - // Store the parent tag and its attributes to be able to restore them later in the button. - // The button block has a wrapper while the paragraph and heading blocks don't. - if ( 'core/button' === $block_name ) { - $button_wrapper = $block_reader->get_tag(); - $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); - $button_wrapper_attrs = array(); - foreach ( $button_wrapper_attribute_names as $name ) { - $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); - } + // Hardcode the selectors and processing until the HTML API is able to read CSS selectors and replace inner HTML. + // TODO: Use the HTML API instead. + if ( 'core/paragraph' === $block_name && 'content' === $attribute_name ) { + $selector = 'p'; } - - foreach ( $selectors as $selector ) { - // If the parent tag, or any of its children, matches the selector, replace the HTML. - if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag( - array( - 'tag_name' => $selector, - ) - ) ) { - $block_reader->release_bookmark( 'iterate-selectors' ); - - // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. - // Until then, it is hardcoded for the paragraph, heading, and button blocks. - // Store the tag and its attributes to be able to restore them later. - $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); - $selector_attrs = array(); - foreach ( $selector_attribute_names as $name ) { - $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); - } - $selector_markup = "<$selector>" . wp_kses_post( $source_value ) . ""; - $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); - $amended_content->next_tag(); - foreach ( $selector_attrs as $attribute_key => $attribute_value ) { - $amended_content->set_attribute( $attribute_key, $attribute_value ); - } - if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { - return $amended_content->get_updated_html(); - } - if ( 'core/button' === $block_name ) { - $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}"; - $amended_button = new WP_HTML_Tag_Processor( $button_markup ); - $amended_button->next_tag(); - foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { - $amended_button->set_attribute( $attribute_key, $attribute_value ); - } - return $amended_button->get_updated_html(); - } + if ( 'core/heading' === $block_name && 'content' === $attribute_name ) { + $selector = 'h[1-6]'; + } + if ( 'core/button' === $block_name && 'text' === $attribute_name ) { + // Check if it is a - + + setAttributes( { feedURL: value } ) + } + className="wp-block-rss__placeholder-input" + /> + diff --git a/packages/block-library/src/rss/editor.scss b/packages/block-library/src/rss/editor.scss index 9b4bac2874ad5..095da46973509 100644 --- a/packages/block-library/src/rss/editor.scss +++ b/packages/block-library/src/rss/editor.scss @@ -14,8 +14,8 @@ } .wp-block-rss__placeholder-input { - flex: 1; - min-width: 80%; + margin: 0 8px 0 0; + flex: 1 1 auto; } } diff --git a/packages/block-library/src/shortcode/edit.js b/packages/block-library/src/shortcode/edit.js index aecebd115a983..b3063077fe308 100644 --- a/packages/block-library/src/shortcode/edit.js +++ b/packages/block-library/src/shortcode/edit.js @@ -4,29 +4,25 @@ import { __ } from '@wordpress/i18n'; import { PlainText, useBlockProps } from '@wordpress/block-editor'; import { useInstanceId } from '@wordpress/compose'; -import { Icon, shortcode } from '@wordpress/icons'; +import { Placeholder } from '@wordpress/components'; +import { shortcode } from '@wordpress/icons'; export default function ShortcodeEdit( { attributes, setAttributes } ) { const instanceId = useInstanceId( ShortcodeEdit ); const inputId = `blocks-shortcode-input-${ instanceId }`; return ( -
- - setAttributes( { text } ) } - /> + <div { ...useBlockProps() }> + <Placeholder icon={ shortcode } label={ __( 'Shortcode' ) }> + <PlainText + className="blocks-shortcode__textarea" + id={ inputId } + value={ attributes.text } + aria-label={ __( 'Shortcode text' ) } + placeholder={ __( 'Write shortcode here…' ) } + onChange={ ( text ) => setAttributes( { text } ) } + /> + </Placeholder> </div> ); } diff --git a/packages/block-library/src/shortcode/editor.scss b/packages/block-library/src/shortcode/editor.scss index 56586df8eefd2..6e7f482ad19f4 100644 --- a/packages/block-library/src/shortcode/editor.scss +++ b/packages/block-library/src/shortcode/editor.scss @@ -1,9 +1,3 @@ -[data-type="core/shortcode"] { - &.components-placeholder { - min-height: 0; - } -} - // The editing view for the Shortcode block is equivalent to block UI. // Therefore we increase specificity to avoid theme styles bleeding in. .blocks-shortcode__textarea { diff --git a/packages/block-library/src/template-part/editor.scss b/packages/block-library/src/template-part/editor.scss index e5df190c2fe85..71659ce6ba717 100644 --- a/packages/block-library/src/template-part/editor.scss +++ b/packages/block-library/src/template-part/editor.scss @@ -24,24 +24,21 @@ } // We don't use .is-outline-mode in this case so colors take effect properly in the block editor. -// Will be a better result when outlines are not shadows, but outlines, so we can target outline-color, not redefine the entire shadow. .block-editor-block-list__block:not(.remove-outline).wp-block-template-part, .block-editor-block-list__block:not(.remove-outline).is-reusable { + &.is-highlighted::after, &.is-selected::after { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-block-synced-color); - } - - &.is-hovered::after { - box-shadow: 0 0 0 $border-width var(--wp-block-synced-color); + outline-color: var(--wp-block-synced-color); } &.block-editor-block-list__block:not([contenteditable]):focus { &::after { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-block-synced-color); + outline-color: var(--wp-block-synced-color); + // Show a light color for dark themes. .is-dark-theme & { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $dark-theme-focus; + outline-color: $dark-theme-focus; } } } diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 8e6fdc9d900db..4086186258573 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -507,7 +507,7 @@ _Parameters_ ### registerBlockStyle -Registers a new block style for the given block. +Registers a new block style for the given block types. For more information on connecting the styles with CSS [the official documentation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/#styles). @@ -536,7 +536,7 @@ const ExampleComponent = () => { _Parameters_ -- _blockName_ `string`: Name of block (example: “core/latest-posts”). +- _blockNames_ `string|Array`: Name of blocks e.g. “core/latest-posts” or `["core/group", "core/columns"]`. - _styleVariation_ `Object`: Object containing `name` which is the class name applied to the block and `label` which identifies the variation to the user. ### registerBlockType diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 68877c280d4dc..620dfcbb8599c 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -51,6 +51,11 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { support: [ 'background', 'backgroundSize' ], useEngine: true, }, + backgroundPosition: { + value: [ 'background', 'backgroundPosition' ], + support: [ 'background', 'backgroundPosition' ], + useEngine: true, + }, borderColor: { value: [ 'border', 'color' ], support: [ '__experimentalBorder', 'color' ], diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 52a2f30983732..fb21b7083b0c5 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -611,13 +611,13 @@ export const hasChildBlocksWithInserterSupport = ( blockName ) => { }; /** - * Registers a new block style for the given block. + * Registers a new block style for the given block types. * * For more information on connecting the styles with CSS * [the official documentation](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/#styles). * - * @param {string} blockName Name of block (example: “core/latest-posts”). - * @param {Object} styleVariation Object containing `name` which is the class name applied to the block and `label` which identifies the variation to the user. + * @param {string|Array} blockNames Name of blocks e.g. “core/latest-posts” or `["core/group", "core/columns"]`. + * @param {Object} styleVariation Object containing `name` which is the class name applied to the block and `label` which identifies the variation to the user. * * @example * ```js @@ -642,8 +642,8 @@ export const hasChildBlocksWithInserterSupport = ( blockName ) => { * }; * ``` */ -export const registerBlockStyle = ( blockName, styleVariation ) => { - dispatch( blocksStore ).addBlockStyles( blockName, styleVariation ); +export const registerBlockStyle = ( blockNames, styleVariation ) => { + dispatch( blocksStore ).addBlockStyles( blockNames, styleVariation ); }; /** diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index d3bd71c067ebe..34216e05808ad 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -98,18 +98,18 @@ export function removeBlockTypes( names ) { * Returns an action object used in signalling that new block styles have been added. * Ignored from documentation as the recommended usage for this action through registerBlockStyle from @wordpress/blocks. * - * @param {string} blockName Block name. - * @param {Array|Object} styles Block style object or array of block style objects. + * @param {string|Array} blockNames Block names to register new styles for. + * @param {Array|Object} styles Block style object or array of block style objects. * * @ignore * * @return {Object} Action object. */ -export function addBlockStyles( blockName, styles ) { +export function addBlockStyles( blockNames, styles ) { return { type: 'ADD_BLOCK_STYLES', styles: Array.isArray( styles ) ? styles : [ styles ], - blockName, + blockNames: Array.isArray( blockNames ) ? blockNames : [ blockNames ], }; } diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 7a3a866485e4a..17af10331e4ed 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -201,13 +201,14 @@ export function blockStyles( state = {}, action ) { ), }; case 'ADD_BLOCK_STYLES': - return { - ...state, - [ action.blockName ]: getUniqueItemsByName( [ - ...( state[ action.blockName ] ?? [] ), + const updatedStyles = {}; + action.blockNames.forEach( ( blockName ) => { + updatedStyles[ blockName ] = getUniqueItemsByName( [ + ...( state[ blockName ] ?? [] ), ...action.styles, - ] ), - }; + ] ); + } ); + return { ...state, ...updatedStyles }; case 'REMOVE_BLOCK_STYLES': return { ...state, diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index 5664f9d876cb6..babaaad4e0e0d 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -10,6 +10,7 @@ import { addBlockVariations, addBlockTypes, removeBlockVariations, + addBlockStyles, } from '../actions'; import { unprocessedBlockTypes, @@ -108,30 +109,55 @@ describe( 'blockStyles', () => { expect( blockStyles( undefined, {} ) ).toEqual( {} ); } ); - it( 'should add a new block styles', () => { + it( 'should add new block styles for a single block type', () => { const original = deepFreeze( {} ); - let state = blockStyles( original, { - type: 'ADD_BLOCK_STYLES', - blockName, - styles: [ { name: 'fancy' } ], - } ); + let state = blockStyles( + original, + addBlockStyles( blockName, [ { name: 'fancy' } ] ) + ); expect( state ).toEqual( { [ blockName ]: [ { name: 'fancy' } ], } ); - state = blockStyles( state, { - type: 'ADD_BLOCK_STYLES', - blockName, - styles: [ { name: 'lightbox' } ], - } ); + state = blockStyles( + state, + addBlockStyles( blockName, [ { name: 'lightbox' } ] ) + ); expect( state ).toEqual( { [ blockName ]: [ { name: 'fancy' }, { name: 'lightbox' } ], } ); } ); + it( 'should add block styles for array of block types', () => { + const original = deepFreeze( {} ); + + let state = blockStyles( + original, + addBlockStyles( + [ 'core/group', 'core/columns' ], + [ { name: 'dark' } ] + ) + ); + + expect( state ).toEqual( { + 'core/group': [ { name: 'dark' } ], + 'core/columns': [ { name: 'dark' } ], + } ); + + state = blockStyles( + state, + addBlockStyles( [ 'core/group' ], [ { name: 'light' } ] ) + ); + + expect( state ).toEqual( { + 'core/group': [ { name: 'dark' }, { name: 'light' } ], + 'core/columns': [ { name: 'dark' } ], + } ); + } ); + it( 'should prepend block styles when adding a block', () => { const original = deepFreeze( { [ blockName ]: [ { name: 'fancy' } ], diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d8263245eebf2..d37feca516b83 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,6 +5,18 @@ ### Internal - Replaced `classnames` package with the faster and smaller `clsx` package ([#61138](https://github.com/WordPress/gutenberg/pull/61138)). +- Upgrade `@use-gesture/react` package to `^10.3.1` ([#61503](https://github.com/WordPress/gutenberg/pull/61503)). +- Upgrade `framer-motion` package to version `^11.1.9` ([#61572](https://github.com/WordPress/gutenberg/pull/61572)). + +### Enhancements + +- `PaletteEdit`: Use consistent spacing and metrics. ([#61368](https://github.com/WordPress/gutenberg/pull/61368)). +- `FormTokenField`: Hide label when not defined ([#61336](https://github.com/WordPress/gutenberg/pull/61336)). +- Upgraded the @types/react and @types/react-dom packages ([#60796](https://github.com/WordPress/gutenberg/pull/60796)). + +### Bug Fix + +- `ToolsPanel`: Fix sticking “Reset” option ([#60621](https://github.com/WordPress/gutenberg/pull/60621)). ## 27.5.0 (2024-05-02) @@ -15,17 +27,15 @@ - `View`: Fix prop types ([#60919](https://github.com/WordPress/gutenberg/pull/60919)). - `Placeholder`: Unify appearance across. ([#59275](https://github.com/WordPress/gutenberg/pull/59275)). - `Toolbar`: Adjust top toolbar to use same metrics as block toolbar ([#61126](https://github.com/WordPress/gutenberg/pull/61126)). +- `DropZone`: Avoid a media query on mount [#60546](https://github.com/WordPress/gutenberg/pull/60546)). +- `ComboboxControl`: Simplify string normalization ([#60893](https://github.com/WordPress/gutenberg/pull/60893)). ### Bug Fix +- `BaseControl`, `InputControl`: Remove usage of aria-details from InputControl and BaseControl ([#61203](https://github.com/WordPress/gutenberg/pull/61203)). - `SlotFill`: fixed missing `getServerSnapshot` parameter in slot map ([#60943](https://github.com/WordPress/gutenberg/pull/60943)). - `Panel`: Fix issue with collapsing panel header ([#61319](https://github.com/WordPress/gutenberg/pull/61319)). -### Enhancements - -- `DropZone`: Avoid a media query on mount [#60546](https://github.com/WordPress/gutenberg/pull/60546)). -- `ComboboxControl`: Simplify string normalization ([#60893](https://github.com/WordPress/gutenberg/pull/60893)). - ### Internal - `FontSizerPicker`: Improve docs for default units ([#60996](https://github.com/WordPress/gutenberg/pull/60996)). diff --git a/packages/components/package.json b/packages/components/package.json index 308765f08a694..cfe046003d945 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -41,7 +41,7 @@ "@floating-ui/react-dom": "^2.0.8", "@types/gradient-parser": "0.1.3", "@types/highlight-words-core": "1.2.1", - "@use-gesture/react": "^10.2.24", + "@use-gesture/react": "^10.3.1", "@wordpress/a11y": "file:../a11y", "@wordpress/compose": "file:../compose", "@wordpress/date": "file:../date", @@ -66,7 +66,7 @@ "deepmerge": "^4.3.0", "downshift": "^6.0.15", "fast-deep-equal": "^3.1.3", - "framer-motion": "^10.13.0", + "framer-motion": "^11.1.9", "gradient-parser": "^0.1.5", "highlight-words-core": "^1.2.2", "is-plain-object": "^5.0.0", diff --git a/packages/components/src/base-control/README.md b/packages/components/src/base-control/README.md index c2f3dc3e0108f..bc46629d2b618 100644 --- a/packages/components/src/base-control/README.md +++ b/packages/components/src/base-control/README.md @@ -53,7 +53,7 @@ If true, the label will only be visible to screen readers. ### help -Additional description for the control. It is preferable to use plain text for `help`, as it can be accessibly associated with the control using `aria-describedby`. When the `help` contains links, or otherwise non-plain text content, it will be associated with the control using `aria-details`. +Additional description for the control. The element containing the description will be programmatically associated to the BaseControl by the means of an `aria-describedby` attribute. - Type: `ReactNode` - Required: No diff --git a/packages/components/src/base-control/hooks.ts b/packages/components/src/base-control/hooks.ts index cfc5a8cf9baa4..7aa7c24257522 100644 --- a/packages/components/src/base-control/hooks.ts +++ b/packages/components/src/base-control/hooks.ts @@ -27,10 +27,6 @@ export function useBaseControlProps( preferredId ); - // ARIA descriptions can only contain plain text, so fall back to aria-details if not. - const helpPropName = - typeof help === 'string' ? 'aria-describedby' : 'aria-details'; - return { baseControlProps: { id: uniqueId, @@ -39,7 +35,7 @@ export function useBaseControlProps( }, controlProps: { id: uniqueId, - ...( !! help ? { [ helpPropName ]: `${ uniqueId }__help` } : {} ), + ...( !! help ? { 'aria-describedby': `${ uniqueId }__help` } : {} ), }, }; } diff --git a/packages/components/src/base-control/test/index.tsx b/packages/components/src/base-control/test/index.tsx index e83c041590c59..07623a8b4c3e7 100644 --- a/packages/components/src/base-control/test/index.tsx +++ b/packages/components/src/base-control/test/index.tsx @@ -31,7 +31,7 @@ describe( 'BaseControl', () => { ).toBeInTheDocument(); } ); - it( 'should render help as aria-details when not plain text', () => { + it( 'should still render help as aria-describedby when not plain text', () => { render( <MyBaseControl label="Text" @@ -44,10 +44,10 @@ describe( 'BaseControl', () => { name: 'My help text', } ); - expect( textarea ).toHaveAttribute( 'aria-details' ); + expect( textarea ).toHaveAttribute( 'aria-describedby' ); expect( // eslint-disable-next-line testing-library/no-node-access - help.closest( `#${ textarea.getAttribute( 'aria-details' ) }` ) + help.closest( `#${ textarea.getAttribute( 'aria-describedby' ) }` ) ).toBeVisible(); } ); } ); diff --git a/packages/components/src/base-control/types.ts b/packages/components/src/base-control/types.ts index 36306e9a24b0b..eeb8736cf1b95 100644 --- a/packages/components/src/base-control/types.ts +++ b/packages/components/src/base-control/types.ts @@ -21,8 +21,7 @@ export type BaseControlProps = { /** * Additional description for the control. * - * It is preferable to use plain text for `help`, as it can be accessibly associated with the control using `aria-describedby`. - * When the `help` contains links, or otherwise non-plain text content, it will be associated with the control using `aria-details`. + * The element containing the description will be programmatically associated to the BaseControl by the means of an `aria-describedby` attribute. */ help?: ReactNode; /** diff --git a/packages/components/src/border-control/types.ts b/packages/components/src/border-control/types.ts index b1ff87aaf5da3..5e028050d8e18 100644 --- a/packages/components/src/border-control/types.ts +++ b/packages/components/src/border-control/types.ts @@ -61,6 +61,10 @@ export type BorderControlProps = ColorProps & * interaction that selects or clears, border color, style, or width. */ onChange: ( value?: Border ) => void; + /** + * Placeholder text for the number input. + */ + placeholder?: HTMLInputElement[ 'placeholder' ]; /** * An internal prop used to control the visibility of the dropdown. */ diff --git a/packages/components/src/focal-point-picker/test/index.js b/packages/components/src/focal-point-picker/test/index.tsx similarity index 75% rename from packages/components/src/focal-point-picker/test/index.js rename to packages/components/src/focal-point-picker/test/index.tsx index d5c7946cffd86..1eccced32c70a 100644 --- a/packages/components/src/focal-point-picker/test/index.js +++ b/packages/components/src/focal-point-picker/test/index.tsx @@ -8,6 +8,19 @@ import userEvent from '@testing-library/user-event'; * Internal dependencies */ import Picker from '..'; +import type { FocalPointPickerProps } from '../types'; + +type Log = { name: string; args: unknown[] }; +type EventLogger = ( name: string, args: unknown[] ) => void; + +const props: FocalPointPickerProps = { + onChange: jest.fn(), + url: 'test-url', + value: { + x: 0, + y: 0, + }, +}; describe( 'FocalPointPicker', () => { describe( 'focus and blur', () => { @@ -16,7 +29,7 @@ describe( 'FocalPointPicker', () => { const mockOnChange = jest.fn(); - render( <Picker onChange={ mockOnChange } /> ); + render( <Picker { ...props } onChange={ mockOnChange } /> ); const draggableArea = screen.getByRole( 'button' ); @@ -32,6 +45,7 @@ describe( 'FocalPointPicker', () => { render( <Picker + { ...props } onChange={ mockOnChange } onDrag={ mockOnDrag } onDragEnd={ mockOnDragEnd } @@ -57,15 +71,16 @@ describe( 'FocalPointPicker', () => { describe( 'drag gestures', () => { it( 'should call onDragStart, onDrag, onDragEnd and onChange in that order', () => { - const logs = []; - const eventLogger = ( name, args ) => logs.push( { name, args } ); + const logs: Log[] = []; + const eventLogger: EventLogger = ( name, args ) => + logs.push( { name, args } ); const events = [ 'onDragStart', 'onDrag', 'onDragEnd', 'onChange' ]; - const handlers = {}; + const handlers: { [ key: string ]: EventLogger } = {}; events.forEach( ( name ) => { handlers[ name ] = ( ...all ) => eventLogger( name, all ); } ); - render( <Picker { ...handlers } /> ); + render( <Picker { ...props } { ...handlers } /> ); const dragArea = screen.getByRole( 'button' ); @@ -92,6 +107,7 @@ describe( 'FocalPointPicker', () => { render( <Picker + { ...props } value={ { x: 0.25, y: 0.25 } } onChange={ spyChange } resolvePoint={ () => { @@ -118,12 +134,12 @@ describe( 'FocalPointPicker', () => { describe( 'controllability', () => { it( 'should update value from props', () => { const { rerender } = render( - <Picker value={ { x: 0.25, y: 0.5 } } /> + <Picker { ...props } value={ { x: 0.25, y: 0.5 } } /> ); const xInput = screen.getByRole( 'spinbutton', { name: 'Focal point left position', - } ); - rerender( <Picker value={ { x: 0.93, y: 0.5 } } /> ); + } ) as HTMLButtonElement; + rerender( <Picker { ...props } value={ { x: 0.93, y: 0.5 } } /> ); expect( xInput.value ).toBe( '93' ); } ); it( 'call onChange with the expected values', async () => { @@ -131,7 +147,11 @@ describe( 'FocalPointPicker', () => { const spyChange = jest.fn(); render( - <Picker value={ { x: 0.14, y: 0.62 } } onChange={ spyChange } /> + <Picker + { ...props } + value={ { x: 0.14, y: 0.62 } } + onChange={ spyChange } + /> ); // Focus and press arrow up const dragArea = screen.getByRole( 'button' ); @@ -151,20 +171,27 @@ describe( 'FocalPointPicker', () => { const onChangeSpy = jest.fn(); render( <Picker - value={ { x: '0.1', y: '0.2' } } + { ...props } + value={ { + x: '0.1' as unknown as number, + y: '0.2' as unknown as number, + } } onChange={ onChangeSpy } /> ); - expect( - screen.getByRole( 'spinbutton', { - name: 'Focal point left position', - } ).value + ( + screen.getByRole( 'spinbutton', { + name: 'Focal point left position', + } ) as HTMLButtonElement + ).value ).toBe( '10' ); expect( - screen.getByRole( 'spinbutton', { - name: 'Focal point top position', - } ).value + ( + screen.getByRole( 'spinbutton', { + name: 'Focal point top position', + } ) as HTMLButtonElement + ).value ).toBe( '20' ); expect( onChangeSpy ).not.toHaveBeenCalled(); } ); diff --git a/packages/components/src/focal-point-picker/test/media.js b/packages/components/src/focal-point-picker/test/media.tsx similarity index 63% rename from packages/components/src/focal-point-picker/test/media.js rename to packages/components/src/focal-point-picker/test/media.tsx index 25296424cf65e..e2c03968f795d 100644 --- a/packages/components/src/focal-point-picker/test/media.js +++ b/packages/components/src/focal-point-picker/test/media.tsx @@ -7,11 +7,22 @@ import { render, screen } from '@testing-library/react'; * Internal dependencies */ import Media from '../media'; +import type { FocalPointPickerMediaProps } from '../types'; + +type FocalPointPickerMediaTestProps = FocalPointPickerMediaProps & { + 'data-testid': string; +}; + +const props: FocalPointPickerMediaTestProps = { + alt: '', + src: '', + 'data-testid': 'media', +}; describe( 'FocalPointPicker/Media', () => { describe( 'Basic rendering', () => { it( 'should render', () => { - render( <Media data-testid="media" /> ); + render( <Media { ...props } /> ); expect( screen.getByTestId( 'media' ) ).toBeVisible(); } ); @@ -19,7 +30,7 @@ describe( 'FocalPointPicker/Media', () => { describe( 'Media types', () => { it( 'should render a placeholder by default', () => { - render( <Media data-testid="media" /> ); + render( <Media { ...props } /> ); expect( screen.getByTestId( 'media' ) ).toHaveClass( 'components-focal-point-picker__media--placeholder' @@ -28,34 +39,28 @@ describe( 'FocalPointPicker/Media', () => { it( 'should render an video if src is a video file', () => { const { rerender } = render( - <Media src="file.mp4" muted={ false } data-testid="media" /> + <Media { ...props } src="file.mp4" muted={ false } /> ); expect( screen.getByTestId( 'media' ).tagName ).toBe( 'VIDEO' ); - rerender( - <Media src="file.ogg" muted={ false } data-testid="media" /> - ); + rerender( <Media { ...props } src="file.ogg" muted={ false } /> ); expect( screen.getByTestId( 'media' ).tagName ).toBe( 'VIDEO' ); - rerender( - <Media src="file.webm" muted={ false } data-testid="media" /> - ); + rerender( <Media { ...props } src="file.webm" muted={ false } /> ); expect( screen.getByTestId( 'media' ).tagName ).toBe( 'VIDEO' ); } ); it( 'should render an image file, if not video', () => { const { rerender } = render( - <Media src="file.gif" data-testid="media" /> + <Media { ...props } src="file.gif" /> ); expect( screen.getByTestId( 'media' ).tagName ).toBe( 'IMG' ); - rerender( - <Media src="file.png" muted={ false } data-testid="media" /> - ); + rerender( <Media { ...props } src="file.png" muted={ false } /> ); expect( screen.getByTestId( 'media' ).tagName ).toBe( 'IMG' ); } ); diff --git a/packages/components/src/form-token-field/index.tsx b/packages/components/src/form-token-field/index.tsx index c7ec536e90eff..d89080b4410c4 100644 --- a/packages/components/src/form-token-field/index.tsx +++ b/packages/components/src/form-token-field/index.tsx @@ -694,12 +694,14 @@ export function FormTokenField( props: FormTokenFieldProps ) { /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( <div { ...tokenFieldProps }> - <StyledLabel - htmlFor={ `components-form-token-input-${ instanceId }` } - className="components-form-token-field__label" - > - { label } - </StyledLabel> + { label && ( + <StyledLabel + htmlFor={ `components-form-token-input-${ instanceId }` } + className="components-form-token-field__label" + > + { label } + </StyledLabel> + ) } <div ref={ tokensAndInput } className={ classes } diff --git a/packages/components/src/input-control/index.tsx b/packages/components/src/input-control/index.tsx index a02c582e1f7fd..a5a9e054bc37d 100644 --- a/packages/components/src/input-control/index.tsx +++ b/packages/components/src/input-control/index.tsx @@ -66,10 +66,7 @@ export function UnforwardedInputControl( onChange, } ); - // ARIA descriptions can only contain plain text, so fall back to aria-details if not. - const helpPropName = - typeof help === 'string' ? 'aria-describedby' : 'aria-details'; - const helpProp = !! help ? { [ helpPropName ]: `${ id }__help` } : {}; + const helpProp = !! help ? { 'aria-describedby': `${ id }__help` } : {}; return ( <BaseControl diff --git a/packages/components/src/input-control/test/index.js b/packages/components/src/input-control/test/index.js index 4a2230bb664fe..ace3086c388c8 100644 --- a/packages/components/src/input-control/test/index.js +++ b/packages/components/src/input-control/test/index.js @@ -53,7 +53,7 @@ describe( 'InputControl', () => { ).toBeInTheDocument(); } ); - it( 'should render help as aria-details when not plain text', () => { + it( 'should still render help as aria-describedby when not plain text', () => { render( <InputControl help={ <a href="/foo">My help text</a> } /> ); const input = screen.getByRole( 'textbox' ); @@ -61,7 +61,7 @@ describe( 'InputControl', () => { expect( // eslint-disable-next-line testing-library/no-node-access - help.closest( `#${ input.getAttribute( 'aria-details' ) }` ) + help.closest( `#${ input.getAttribute( 'aria-describedby' ) }` ) ).toBeVisible(); } ); } ); diff --git a/packages/components/src/palette-edit/index.tsx b/packages/components/src/palette-edit/index.tsx index 01d9aa76de629..8d8e00888dd56 100644 --- a/packages/components/src/palette-edit/index.tsx +++ b/packages/components/src/palette-edit/index.tsx @@ -37,13 +37,13 @@ import { PaletteActionsContainer, PaletteEditStyles, PaletteHeading, - PaletteHStackHeader, IndicatorStyled, PaletteItem, NameContainer, NameInputControl, DoneButton, RemoveButton, + PaletteEditContents, } from './styles'; import { NavigableMenu } from '../navigable-container'; import { DEFAULT_GRADIENT } from '../custom-gradient-picker/constants'; @@ -410,7 +410,7 @@ export function PaletteEdit( { return ( <PaletteEditStyles> - <PaletteHStackHeader> + <HStack> <PaletteHeading level={ paletteLabelHeadingLevel }> { paletteLabel } </PaletteHeading> @@ -542,9 +542,9 @@ export function PaletteEdit( { </DropdownMenu> ) } </PaletteActionsContainer> - </PaletteHStackHeader> + </HStack> { hasElements && ( - <> + <PaletteEditContents> { isEditing && ( <PaletteEditListView< ( typeof elements )[ number ] > canOnlyChangeValues={ canOnlyChangeValues } @@ -602,9 +602,11 @@ export function PaletteEdit( { disableCustomColors /> ) ) } - </> + </PaletteEditContents> + ) } + { ! hasElements && emptyMessage && ( + <PaletteEditContents>{ emptyMessage }</PaletteEditContents> ) } - { ! hasElements && emptyMessage } </PaletteEditStyles> ); } diff --git a/packages/components/src/palette-edit/styles.ts b/packages/components/src/palette-edit/styles.ts index f0d8301309557..aa4ed720b93bf 100644 --- a/packages/components/src/palette-edit/styles.ts +++ b/packages/components/src/palette-edit/styles.ts @@ -9,7 +9,6 @@ import { css } from '@emotion/react'; */ import Button from '../button'; import { Heading } from '../heading'; -import { HStack } from '../h-stack'; import { space } from '../utils/space'; import { COLORS, CONFIG, font } from '../utils'; import { View } from '../view'; @@ -131,8 +130,8 @@ export const PaletteActionsContainer = styled( View )` display: flex; `; -export const PaletteHStackHeader = styled( HStack )` - margin-bottom: ${ space( 2 ) }; +export const PaletteEditContents = styled( View )` + margin-top: ${ space( 2 ) }; `; export const PaletteEditStyles = styled( View )` diff --git a/packages/components/src/theme/stories/index.story.tsx b/packages/components/src/theme/stories/index.story.tsx index c26e0a752c0d4..15570f7ded1da 100644 --- a/packages/components/src/theme/stories/index.story.tsx +++ b/packages/components/src/theme/stories/index.story.tsx @@ -13,7 +13,7 @@ import { HStack } from '../../h-stack'; const meta: Meta< typeof Theme > = { component: Theme, - title: 'Components (Experimental)/Theme', + title: 'Components/Theme', argTypes: { accent: { control: { type: 'color' } }, background: { control: { type: 'color' } }, diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index edd203a432199..a7adcc81aa735 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -278,7 +278,6 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = <div class="emotion-15" role="presentation" - style="opacity: 1;" /> </div> </div> @@ -824,7 +823,6 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] <div class="emotion-15" role="presentation" - style="opacity: 1;" /> </div> </div> diff --git a/packages/components/src/tools-panel/tools-panel-item/hook.ts b/packages/components/src/tools-panel/tools-panel-item/hook.ts index fe415b8723a88..1e33e7c6740de 100644 --- a/packages/components/src/tools-panel/tools-panel-item/hook.ts +++ b/packages/components/src/tools-panel/tools-panel-item/hook.ts @@ -125,17 +125,21 @@ export function useToolsPanelItem( const isRegistered = menuItems?.[ menuGroup ]?.[ label ] !== undefined; const isValueSet = hasValue(); - const wasValueSet = usePrevious( isValueSet ); - const newValueSet = isValueSet && ! wasValueSet; - - // Notify the panel when an item's value has been set. + // Notify the panel when an item's value has changed except for optional + // items without value because the item should not cause itself to hide. useEffect( () => { - if ( ! newValueSet ) { + if ( ! isShownByDefault && ! isValueSet ) { return; } - flagItemCustomization( label, menuGroup ); - }, [ newValueSet, menuGroup, label, flagItemCustomization ] ); + flagItemCustomization( isValueSet, label, menuGroup ); + }, [ + isValueSet, + menuGroup, + label, + flagItemCustomization, + isShownByDefault, + ] ); // Determine if the panel item's corresponding menu is being toggled and // trigger appropriate callback if it is. @@ -151,7 +155,7 @@ export function useToolsPanelItem( onSelect?.(); } - if ( ! isMenuItemChecked && wasMenuItemChecked ) { + if ( ! isMenuItemChecked && isValueSet && wasMenuItemChecked ) { onDeselect?.(); } }, [ diff --git a/packages/components/src/tools-panel/tools-panel/hook.ts b/packages/components/src/tools-panel/tools-panel/hook.ts index 8a38a15084b33..8742f1c494ce1 100644 --- a/packages/components/src/tools-panel/tools-panel/hook.ts +++ b/packages/components/src/tools-panel/tools-panel/hook.ts @@ -205,18 +205,21 @@ export function useToolsPanel( } ); }, [ panelItems, setMenuItems, menuItemOrder ] ); - // Force a menu item to be checked. - // This is intended for use with default panel items. They are displayed - // separately to optional items and have different display states, - // we need to update that when their value is customized. + // Updates the status of the panel’s menu items. For default items the + // value represents whether it differs from the default and for optional + // items whether the item is shown. const flagItemCustomization = useCallback( - ( label: string, group: ToolsPanelMenuItemKey = 'default' ) => { + ( + value: boolean, + label: string, + group: ToolsPanelMenuItemKey = 'default' + ) => { setMenuItems( ( items ) => { const newState = { ...items, [ group ]: { ...items[ group ], - [ label ]: true, + [ label ]: value, }, }; return newState; diff --git a/packages/components/src/tools-panel/types.ts b/packages/components/src/tools-panel/types.ts index 9f4fc78bea46a..e8e2f950de9a3 100644 --- a/packages/components/src/tools-panel/types.ts +++ b/packages/components/src/tools-panel/types.ts @@ -176,6 +176,7 @@ export type ToolsPanelContext = { registerResetAllFilter: ( filter: ResetAllFilter ) => void; deregisterResetAllFilter: ( filter: ResetAllFilter ) => void; flagItemCustomization: ( + value: boolean, label: string, group?: ToolsPanelMenuItemKey ) => void; diff --git a/packages/components/src/truncate/hook.ts b/packages/components/src/truncate/hook.ts index 440865a596432..940e3b0127b08 100644 --- a/packages/components/src/truncate/hook.ts +++ b/packages/components/src/truncate/hook.ts @@ -57,7 +57,7 @@ export default function useTruncate( // breaks even when it contains 'unbreakable' content such as long URLs. // See https://github.com/WordPress/gutenberg/issues/60860. const truncateLines = css` - word-break: break-all; + ${ numberOfLines === 1 ? 'word-break: break-all;' : '' } -webkit-box-orient: vertical; -webkit-line-clamp: ${ numberOfLines }; display: -webkit-box; diff --git a/packages/core-commands/src/admin-navigation-commands.js b/packages/core-commands/src/admin-navigation-commands.js index 4de403761f4cc..0ffa7ba7eb628 100644 --- a/packages/core-commands/src/admin-navigation-commands.js +++ b/packages/core-commands/src/admin-navigation-commands.js @@ -3,26 +3,9 @@ */ import { useCommand } from '@wordpress/commands'; import { __ } from '@wordpress/i18n'; -import { plus, symbol } from '@wordpress/icons'; -import { addQueryArgs, getPath } from '@wordpress/url'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import { useIsTemplatesAccessible } from './hooks'; -import { unlock } from './lock-unlock'; - -const { useHistory } = unlock( routerPrivateApis ); +import { plus } from '@wordpress/icons'; export function useAdminNavigationCommands() { - const history = useHistory(); - const isTemplatesAccessible = useIsTemplatesAccessible(); - - const isSiteEditor = getPath( window.location.href )?.includes( - 'site-editor.php' - ); - useCommand( { name: 'core/add-new-post', label: __( 'Add new post' ), @@ -39,28 +22,4 @@ export function useAdminNavigationCommands() { document.location.href = 'post-new.php?post_type=page'; }, } ); - useCommand( { - name: 'core/manage-reusable-blocks', - label: __( 'Patterns' ), - icon: symbol, - callback: ( { close } ) => { - // The site editor and templates both check whether the user - // can read templates. We can leverage that here and this - // command links to the classic dashboard manage patterns page - // if the user can't access it. - if ( isTemplatesAccessible ) { - const args = { - path: '/patterns', - }; - if ( isSiteEditor ) { - history.push( args ); - } else { - document.location = addQueryArgs( 'site-editor.php', args ); - } - close(); - } else { - document.location.href = 'edit.php?post_type=wp_block'; - } - }, - } ); } diff --git a/packages/core-commands/src/hooks.js b/packages/core-commands/src/hooks.js index 6d744e3223234..0622a970680c1 100644 --- a/packages/core-commands/src/hooks.js +++ b/packages/core-commands/src/hooks.js @@ -4,13 +4,6 @@ import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; -export function useIsTemplatesAccessible() { - return useSelect( - ( select ) => select( coreStore ).canUser( 'read', 'templates' ), - [] - ); -} - export function useIsBlockBasedTheme() { return useSelect( ( select ) => select( coreStore ).getCurrentTheme()?.is_block_theme, diff --git a/packages/core-commands/src/site-editor-navigation-commands.js b/packages/core-commands/src/site-editor-navigation-commands.js index 695ad00e56720..410a3e875a8df 100644 --- a/packages/core-commands/src/site-editor-navigation-commands.js +++ b/packages/core-commands/src/site-editor-navigation-commands.js @@ -10,6 +10,7 @@ import { post, page, layout, + symbol, symbolFilled, styles, navigation, @@ -21,7 +22,7 @@ import { useDebounce } from '@wordpress/compose'; /** * Internal dependencies */ -import { useIsTemplatesAccessible, useIsBlockBasedTheme } from './hooks'; +import { useIsBlockBasedTheme } from './hooks'; import { unlock } from './lock-unlock'; import { orderEntityRecordsBySearch } from './utils/order-entity-records-by-search'; @@ -196,31 +197,67 @@ const getNavigationCommandLoaderPerTemplate = ( templateType ) => ) { return []; } - return orderedRecords.map( ( record ) => { - const isSiteEditor = getPath( window.location.href )?.includes( - 'site-editor.php' - ); - const extraArgs = isSiteEditor - ? { canvas: getQueryArg( window.location.href, 'canvas' ) } - : {}; + const isSiteEditor = getPath( window.location.href )?.includes( + 'site-editor.php' + ); + const result = []; + result.push( + ...orderedRecords.map( ( record ) => { + const extraArgs = isSiteEditor + ? { + canvas: getQueryArg( + window.location.href, + 'canvas' + ), + } + : {}; - return { - name: templateType + '-' + record.id, - searchLabel: record.title?.rendered + ' ' + record.id, - label: record.title?.rendered - ? record.title?.rendered - : __( '(no title)' ), - icon: icons[ templateType ], + return { + name: templateType + '-' + record.id, + searchLabel: record.title?.rendered + ' ' + record.id, + label: record.title?.rendered + ? record.title?.rendered + : __( '(no title)' ), + icon: icons[ templateType ], + callback: ( { close } ) => { + const args = { + postType: templateType, + postId: record.id, + didAccessPatternsPage: + ! isBlockBasedTheme && + ( isPatternsPage || didAccessPatternsPage ) + ? 1 + : undefined, + ...extraArgs, + }; + const targetUrl = addQueryArgs( + 'site-editor.php', + args + ); + if ( isSiteEditor ) { + history.push( args ); + } else { + document.location = targetUrl; + } + close(); + }, + }; + } ) + ); + + if ( + orderedRecords?.length > 0 && + templateType === 'wp_template_part' + ) { + result.push( { + name: 'core/edit-site/open-template-parts', + label: __( 'Template parts' ), + icon: symbolFilled, callback: ( { close } ) => { const args = { - postType: templateType, - postId: record.id, - didAccessPatternsPage: - ! isBlockBasedTheme && - ( isPatternsPage || didAccessPatternsPage ) - ? 1 - : undefined, - ...extraArgs, + path: '/patterns', + categoryType: 'wp_template_part', + categoryId: 'all-parts', }; const targetUrl = addQueryArgs( 'site-editor.php', @@ -233,8 +270,9 @@ const getNavigationCommandLoaderPerTemplate = ( templateType ) => } close(); }, - }; - } ); + } ); + } + return result; }, [ isBlockBasedTheme, orderedRecords, history ] ); return { @@ -257,89 +295,112 @@ function useSiteEditorBasicNavigationCommands() { const isSiteEditor = getPath( window.location.href )?.includes( 'site-editor.php' ); - const isTemplatesAccessible = useIsTemplatesAccessible(); + const canCreateTemplate = useSelect( ( select ) => { + return select( coreStore ).canUser( 'create', 'templates' ); + }, [] ); const isBlockBasedTheme = useIsBlockBasedTheme(); const commands = useMemo( () => { const result = []; - if ( ! isTemplatesAccessible || ! isBlockBasedTheme ) { - return result; - } + if ( canCreateTemplate && isBlockBasedTheme ) { + result.push( { + name: 'core/edit-site/open-navigation', + label: __( 'Navigation' ), + icon: navigation, + callback: ( { close } ) => { + const args = { + path: '/navigation', + }; + const targetUrl = addQueryArgs( 'site-editor.php', args ); + if ( isSiteEditor ) { + history.push( args ); + } else { + document.location = targetUrl; + } + close(); + }, + } ); - result.push( { - name: 'core/edit-site/open-navigation', - label: __( 'Navigation' ), - icon: navigation, - callback: ( { close } ) => { - const args = { - path: '/navigation', - }; - const targetUrl = addQueryArgs( 'site-editor.php', args ); - if ( isSiteEditor ) { - history.push( args ); - } else { - document.location = targetUrl; - } - close(); - }, - } ); + result.push( { + name: 'core/edit-site/open-styles', + label: __( 'Styles' ), + icon: styles, + callback: ( { close } ) => { + const args = { + path: '/wp_global_styles', + }; + const targetUrl = addQueryArgs( 'site-editor.php', args ); + if ( isSiteEditor ) { + history.push( args ); + } else { + document.location = targetUrl; + } + close(); + }, + } ); - result.push( { - name: 'core/edit-site/open-styles', - label: __( 'Styles' ), - icon: styles, - callback: ( { close } ) => { - const args = { - path: '/wp_global_styles', - }; - const targetUrl = addQueryArgs( 'site-editor.php', args ); - if ( isSiteEditor ) { - history.push( args ); - } else { - document.location = targetUrl; - } - close(); - }, - } ); + result.push( { + name: 'core/edit-site/open-pages', + label: __( 'Pages' ), + icon: page, + callback: ( { close } ) => { + const args = { + path: '/page', + }; + const targetUrl = addQueryArgs( 'site-editor.php', args ); + if ( isSiteEditor ) { + history.push( args ); + } else { + document.location = targetUrl; + } + close(); + }, + } ); - result.push( { - name: 'core/edit-site/open-pages', - label: __( 'Pages' ), - icon: page, - callback: ( { close } ) => { - const args = { - path: '/page', - }; - const targetUrl = addQueryArgs( 'site-editor.php', args ); - if ( isSiteEditor ) { - history.push( args ); - } else { - document.location = targetUrl; - } - close(); - }, - } ); + result.push( { + name: 'core/edit-site/open-templates', + label: __( 'Templates' ), + icon: layout, + callback: ( { close } ) => { + const args = { + path: '/wp_template', + }; + const targetUrl = addQueryArgs( 'site-editor.php', args ); + if ( isSiteEditor ) { + history.push( args ); + } else { + document.location = targetUrl; + } + close(); + }, + } ); + } result.push( { - name: 'core/edit-site/open-templates', - label: __( 'Templates' ), - icon: layout, + name: 'core/edit-site/open-patterns', + label: __( 'Patterns' ), + icon: symbol, callback: ( { close } ) => { - const args = { - path: '/wp_template', - }; - const targetUrl = addQueryArgs( 'site-editor.php', args ); - if ( isSiteEditor ) { - history.push( args ); + if ( canCreateTemplate ) { + const args = { + path: '/patterns', + }; + const targetUrl = addQueryArgs( 'site-editor.php', args ); + if ( isSiteEditor ) { + history.push( args ); + } else { + document.location = targetUrl; + } + close(); } else { - document.location = targetUrl; + // If a user cannot access the site editor + document.location.href = 'edit.php?post_type=wp_block'; } - close(); }, } ); return result; - }, [ history, isSiteEditor, isTemplatesAccessible, isBlockBasedTheme ] ); + }, [ history, isSiteEditor, canCreateTemplate, isBlockBasedTheme ] ); return { commands, diff --git a/packages/create-block/README.md b/packages/create-block/README.md index ab80121382073..55cea45c9afe7 100644 --- a/packages/create-block/README.md +++ b/packages/create-block/README.md @@ -19,7 +19,7 @@ $ npm start The `slug` provided (`todo-list` in the example) defines the folder name for the scaffolded plugin and the internal block name. The WordPress plugin generated must [be installed manually](https://wordpress.org/documentation/article/manage-plugins/#manual-plugin-installation-1). -_(requires `node` version `18.0.0` or above, and `npm` version `10.5.0` or above)_ +_(requires `node` version `20.10.0` or above, and `npm` version `10.2.3` or above)_ > [Watch a video introduction to create-block on Learn.wordpress.org](https://learn.wordpress.org/tutorial/using-the-create-block-tool/) diff --git a/packages/create-block/package.json b/packages/create-block/package.json index 2742e0b8f7c1d..b1f7095ea6f13 100644 --- a/packages/create-block/package.json +++ b/packages/create-block/package.json @@ -20,8 +20,8 @@ "url": "https://github.com/WordPress/gutenberg/issues" }, "engines": { - "node": ">=18", - "npm": ">=10.5.0" + "node": ">=20.10.0", + "npm": ">=10.2.3" }, "files": [ "lib" diff --git a/packages/dataviews/src/bulk-actions-toolbar.js b/packages/dataviews/src/bulk-actions-toolbar.js new file mode 100644 index 0000000000000..3df9b64192fd3 --- /dev/null +++ b/packages/dataviews/src/bulk-actions-toolbar.js @@ -0,0 +1,244 @@ +/** + * WordPress dependencies + */ +import { + ToolbarButton, + Toolbar, + ToolbarGroup, + __unstableMotion as motion, + __unstableAnimatePresence as AnimatePresence, +} from '@wordpress/components'; +import { useMemo, useState, useRef } from '@wordpress/element'; +import { _n, sprintf, __ } from '@wordpress/i18n'; +import { closeSmall } from '@wordpress/icons'; +import { useReducedMotion } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { ActionWithModal } from './item-actions'; + +const SNACKBAR_VARIANTS = { + init: { + bottom: -48, + }, + open: { + bottom: 24, + transition: { + bottom: { type: 'tween', duration: 0.2, ease: [ 0, 0, 0.2, 1 ] }, + }, + }, + exit: { + opacity: 0, + bottom: 24, + transition: { + opacity: { type: 'tween', duration: 0.2, ease: [ 0, 0, 0.2, 1 ] }, + }, + }, +}; + +function ActionTrigger( { action, onClick, isBusy } ) { + return ( + <ToolbarButton + disabled={ isBusy } + label={ action.label } + icon={ action.icon } + isDestructive={ action.isDestructive } + size="compact" + onClick={ onClick } + isBusy={ isBusy } + __experimentalIsFocusable + tooltipPosition="top" + /> + ); +} + +const EMPTY_ARRAY = []; + +function ActionButton( { + action, + selectedItems, + actionInProgress, + setActionInProgress, +} ) { + const selectedEligibleItems = useMemo( () => { + return selectedItems.filter( ( item ) => { + return action.isEligible( item ); + } ); + }, [ action, selectedItems ] ); + if ( !! action.RenderModal ) { + return ( + <ActionWithModal + key={ action.id } + action={ action } + items={ selectedEligibleItems } + ActionTrigger={ ActionTrigger } + onActionStart={ () => { + setActionInProgress( action.id ); + } } + onActionPerformed={ () => { + setActionInProgress( null ); + } } + /> + ); + } + return ( + <ActionTrigger + key={ action.id } + action={ action } + items={ selectedItems } + onClick={ () => { + setActionInProgress( action.id ); + action.callback( selectedItems, () => { + setActionInProgress( action.id ); + } ); + } } + isBusy={ actionInProgress === action.id } + /> + ); +} + +function renderToolbarContent( + selection, + actionsToShow, + selectedItems, + actionInProgress, + setActionInProgress, + setSelection +) { + return ( + <> + <ToolbarGroup> + <div className="dataviews-bulk-actions__selection-count"> + { selection.length === 1 + ? __( '1 item selected' ) + : sprintf( + // translators: %s: Total number of selected items. + _n( + '%s item selected', + '%s items selected', + selection.length + ), + selection.length + ) } + </div> + </ToolbarGroup> + <ToolbarGroup> + { actionsToShow.map( ( action ) => { + return ( + <ActionButton + key={ action.id } + action={ action } + selectedItems={ selectedItems } + actionInProgress={ actionInProgress } + setActionInProgress={ setActionInProgress } + /> + ); + } ) } + </ToolbarGroup> + <ToolbarGroup> + <ToolbarButton + icon={ closeSmall } + showTooltip + tooltipPosition="top" + label={ __( 'Cancel' ) } + disabled={ !! actionInProgress } + onClick={ () => { + setSelection( EMPTY_ARRAY ); + } } + /> + </ToolbarGroup> + </> + ); +} + +function ToolbarContent( { + selection, + actionsToShow, + selectedItems, + setSelection, +} ) { + const [ actionInProgress, setActionInProgress ] = useState( null ); + const buttons = useRef( null ); + if ( ! actionInProgress ) { + if ( buttons.current ) { + buttons.current = null; + } + return renderToolbarContent( + selection, + actionsToShow, + selectedItems, + actionInProgress, + setActionInProgress, + setSelection + ); + } else if ( ! buttons.current ) { + buttons.current = renderToolbarContent( + selection, + actionsToShow, + selectedItems, + actionInProgress, + setActionInProgress, + setSelection + ); + } + return buttons.current; +} + +export default function BulkActionsToolbar( { + data, + selection, + actions = EMPTY_ARRAY, + setSelection, + getItemId, +} ) { + const isReducedMotion = useReducedMotion(); + const selectedItems = useMemo( () => { + return data.filter( ( item ) => + selection.includes( getItemId( item ) ) + ); + }, [ selection, data, getItemId ] ); + + const actionsToShow = useMemo( + () => + actions.filter( ( action ) => { + return ( + action.supportsBulk && + action.icon && + selectedItems.some( ( item ) => action.isEligible( item ) ) + ); + } ), + [ actions, selectedItems ] + ); + + if ( + ( selection && selection.length === 0 ) || + actionsToShow.length === 0 + ) { + return null; + } + + return ( + <AnimatePresence> + <motion.div + layout={ ! isReducedMotion } // See https://www.framer.com/docs/animation/#layout-animations + initial={ 'init' } + animate={ 'open' } + exit={ 'exit' } + variants={ isReducedMotion ? undefined : SNACKBAR_VARIANTS } + className="dataviews-bulk-actions" + > + <Toolbar label={ __( 'Bulk actions' ) }> + <div className="dataviews-bulk-actions-toolbar-wrapper"> + <ToolbarContent + selection={ selection } + actionsToShow={ actionsToShow } + selectedItems={ selectedItems } + setSelection={ setSelection } + /> + </div> + </Toolbar> + </motion.div> + </AnimatePresence> + ); +} diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index d8d6281af9498..33f0da08c0ba5 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -15,6 +15,7 @@ import { LAYOUT_TABLE, LAYOUT_GRID } from './constants'; import { VIEW_LAYOUTS } from './layouts'; import BulkActions from './bulk-actions'; import { normalizeFields } from './normalize-fields'; +import BulkActionsToolbar from './bulk-actions-toolbar'; const defaultGetItemId = ( item ) => item.id; const defaultOnSelectionChange = () => {}; @@ -143,6 +144,16 @@ export default function DataViews( { onChangeView={ onChangeView } paginationInfo={ paginationInfo } /> + { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) && + hasPossibleBulkAction && ( + <BulkActionsToolbar + data={ data } + actions={ actions } + selection={ selection } + setSelection={ setSelection } + getItemId={ getItemId } + /> + ) } </div> ); } diff --git a/packages/dataviews/src/item-actions.js b/packages/dataviews/src/item-actions.js index db4da0d492489..2d928cdbd451b 100644 --- a/packages/dataviews/src/item-actions.js +++ b/packages/dataviews/src/item-actions.js @@ -47,11 +47,22 @@ function DropdownMenuItemTrigger( { action, onClick } ) { ); } -function ActionWithModal( { action, item, ActionTrigger } ) { +export function ActionWithModal( { + action, + items, + ActionTrigger, + onActionStart, + onActionPerformed, + isBusy, +} ) { const [ isModalOpen, setIsModalOpen ] = useState( false ); const actionTriggerProps = { action, - onClick: () => setIsModalOpen( true ), + onClick: () => { + setIsModalOpen( true ); + }, + items, + isBusy, }; const { RenderModal, hideModalHeader } = action; return ( @@ -69,8 +80,10 @@ function ActionWithModal( { action, item, ActionTrigger } ) { ) }` } > <RenderModal - items={ [ item ] } + items={ items } closeModal={ () => setIsModalOpen( false ) } + onActionStart={ onActionStart } + onActionPerformed={ onActionPerformed } /> </Modal> ) } @@ -87,7 +100,7 @@ function ActionsDropdownMenuGroup( { actions, item } ) { <ActionWithModal key={ action.id } action={ action } - item={ item } + items={ [ item ] } ActionTrigger={ DropdownMenuItemTrigger } /> ); @@ -139,7 +152,7 @@ export default function ItemActions( { item, actions, isCompact } ) { <ActionWithModal key={ action.id } action={ action } - item={ item } + items={ [ item ] } ActionTrigger={ ButtonTrigger } /> ); diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 6efa7884c8adf..e8e8973ebf679 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -783,3 +783,48 @@ } } } + + +.dataviews-bulk-actions-toolbar-wrapper { + display: flex; + flex-grow: 1; + width: 100%; + + .components-toolbar-group { + align-items: center; + } + + .components-button.is-busy { + max-height: $button-size; + } +} + +.dataviews-bulk-actions { + position: absolute; + display: flex; + flex-direction: column; + align-content: center; + flex-wrap: wrap; + width: 100%; + bottom: $grid-unit-30; + z-index: z-index(".dataviews-bulk-actions"); + + .components-accessible-toolbar { + border-color: $gray-300; + box-shadow: $shadow-popover; + + .components-toolbar-group { + border-color: $gray-200; + + &:last-child { + border: 0; + } + } + } + + .dataviews-bulk-actions__selection-count { + display: flex; + align-items: center; + margin: 0 $grid-unit-10 0 $grid-unit-10; + } +} diff --git a/packages/dataviews/src/view-list.tsx b/packages/dataviews/src/view-list.tsx index d88f11de59eed..193e89012fb89 100644 --- a/packages/dataviews/src/view-list.tsx +++ b/packages/dataviews/src/view-list.tsx @@ -40,7 +40,7 @@ interface ListViewProps { } interface ListViewItemProps { - id: string; + id?: string; item: Item; isSelected: boolean; onSelect: ( item: Item ) => void; @@ -183,7 +183,8 @@ export default function ViewList( props: ListViewProps ) { ); const getItemDomId = useCallback( - ( item?: Item ) => ( item ? `${ baseId }-${ getItemId( item ) }` : '' ), + ( item?: Item ) => + item ? `${ baseId }-${ getItemId( item ) }` : undefined, [ baseId, getItemId ] ); diff --git a/packages/e2e-test-utils-playwright/src/editor/publish-post.ts b/packages/e2e-test-utils-playwright/src/editor/publish-post.ts index bf4b240a3fd61..81451cd516f0e 100644 --- a/packages/e2e-test-utils-playwright/src/editor/publish-post.ts +++ b/packages/e2e-test-utils-playwright/src/editor/publish-post.ts @@ -16,7 +16,7 @@ export async function publishPost( this: Editor ) { .getByRole( 'button', { name: 'Save', exact: true } ); const publishButton = this.page .getByRole( 'region', { name: 'Editor top bar' } ) - .getByRole( 'button', { name: 'Publish' } ); + .getByRole( 'button', { name: 'Publish', exact: true } ); const buttonToClick = ( await saveButton.isVisible() ) ? saveButton : publishButton; diff --git a/packages/e2e-test-utils-playwright/src/test.ts b/packages/e2e-test-utils-playwright/src/test.ts index 677eff31955bd..6ef614024d7d9 100644 --- a/packages/e2e-test-utils-playwright/src/test.ts +++ b/packages/e2e-test-utils-playwright/src/test.ts @@ -60,7 +60,12 @@ function observeConsoleLogging( message: ConsoleMessage ) { // See: https://core.trac.wordpress.org/ticket/37000 // See: https://www.chromestatus.com/feature/5088147346030592 // See: https://www.chromestatus.com/feature/5633521622188032 - if ( text.includes( 'A cookie associated with a cross-site resource' ) ) { + if ( + text.includes( 'A cookie associated with a cross-site resource' ) || + text.includes( + 'https://developer.mozilla.org/docs/Web/HTTP/Headers/Set-Cookie/SameSite' + ) + ) { return; } @@ -93,6 +98,18 @@ function observeConsoleLogging( message: ConsoleMessage ) { return; } + // https://bugzilla.mozilla.org/show_bug.cgi?id=1404468 + if ( + text.includes( 'Layout was forced before the page was fully loaded' ) + ) { + return; + } + + // Deprecated warnings coming from the third-party libraries. + if ( text.includes( 'MouseEvent.moz' ) ) { + return; + } + const logFunction = type as ( typeof OBSERVED_CONSOLE_MESSAGE_TYPES )[ number ]; @@ -100,7 +117,6 @@ function observeConsoleLogging( message: ConsoleMessage ) { // which, unless the test explicitly anticipates the logging via // @wordpress/jest-console matchers, will cause the intended test // failure. - // eslint-disable-next-line no-console console[ logFunction ]( text ); } diff --git a/packages/e2e-test-utils/src/ensure-sidebar-opened.js b/packages/e2e-test-utils/src/ensure-sidebar-opened.js index 5ea99c629c15e..95c674e980643 100644 --- a/packages/e2e-test-utils/src/ensure-sidebar-opened.js +++ b/packages/e2e-test-utils/src/ensure-sidebar-opened.js @@ -8,7 +8,8 @@ export async function ensureSidebarOpened() { '.edit-post-header__settings [aria-label="Settings"][aria-expanded="false"],' + '.edit-site-header__actions [aria-label="Settings"][aria-expanded="false"],' + '.edit-widgets-header__actions [aria-label="Settings"][aria-expanded="false"],' + - '.edit-site-header-edit-mode__actions [aria-label="Settings"][aria-expanded="false"]' + '.edit-site-header-edit-mode__actions [aria-label="Settings"][aria-expanded="false"],' + + '.editor-header__settings [aria-label="Settings"][aria-expanded="false"]' ); if ( toggleSidebarButton ) { diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js index cf5d7c48c9dfd..5beab3c6205b6 100644 --- a/packages/e2e-test-utils/src/inserter.js +++ b/packages/e2e-test-utils/src/inserter.js @@ -53,7 +53,8 @@ async function isGlobalInserterOpen() { '.edit-site-header [aria-label="Toggle block inserter"].is-pressed,' + '.edit-widgets-header [aria-label="Toggle block inserter"].is-pressed,' + '.edit-widgets-header [aria-label="Add block"].is-pressed,' + - '.edit-site-header-edit-mode__inserter-toggle.is-pressed' + '.edit-site-header-edit-mode__inserter-toggle.is-pressed,' + + '.editor-header [aria-label="Toggle block inserter"].is-pressed' ); } ); } diff --git a/packages/e2e-test-utils/src/site-editor.js b/packages/e2e-test-utils/src/site-editor.js index 4f6cf1773134f..98ba34f7db4f5 100644 --- a/packages/e2e-test-utils/src/site-editor.js +++ b/packages/e2e-test-utils/src/site-editor.js @@ -97,9 +97,7 @@ export async function visitSiteEditor( query, skipWelcomeGuide = true ) { * Toggles the global styles sidebar (opens it if closed and closes it if open). */ export async function toggleGlobalStyles() { - await page.click( - '.edit-site-header-edit-mode__actions button[aria-label="Styles"]' - ); + await page.click( '.editor-header__settings button[aria-label="Styles"]' ); } /** diff --git a/packages/e2e-tests/mu-plugins/server-timing.php b/packages/e2e-tests/mu-plugins/server-timing.php new file mode 100644 index 0000000000000..84771f980ff7f --- /dev/null +++ b/packages/e2e-tests/mu-plugins/server-timing.php @@ -0,0 +1,91 @@ +<?php + +add_filter( + 'template_include', + static function ( $template ) { + + global $timestart, $wpdb; + + $server_timing_values = array(); + $template_start = microtime( true ); + + $server_timing_values['wpBeforeTemplate'] = $template_start - $timestart; + + ob_start(); + + add_action( + 'shutdown', + static function () use ( $server_timing_values, $template_start, $wpdb ) { + $output = ob_get_clean(); + + $server_timing_values['wpTemplate'] = microtime( true ) - $template_start; + + $server_timing_values['wpTotal'] = $server_timing_values['wpBeforeTemplate'] + $server_timing_values['wpTemplate']; + + /* + * While values passed via Server-Timing are intended to be durations, + * any numeric value can actually be passed. + * This is a nice little trick as it allows to easily get this information in JS. + */ + $server_timing_values['wpMemoryUsage'] = memory_get_usage(); + $server_timing_values['wpDbQueries'] = $wpdb->num_queries; + + $header_values = array(); + foreach ( $server_timing_values as $slug => $value ) { + if ( is_float( $value ) ) { + $value = round( $value * 1000.0, 2 ); + } + $header_values[] = sprintf( '%1$s;dur=%2$s', $slug, $value ); + } + header( 'Server-Timing: ' . implode( ', ', $header_values ) ); + + echo $output; + }, + PHP_INT_MIN + ); + + return $template; + }, + PHP_INT_MAX +); + +add_action( + 'admin_init', + static function () { + global $timestart, $wpdb; + + ob_start(); + + add_action( + 'shutdown', + static function () use ( $wpdb, $timestart ) { + $output = ob_get_clean(); + + $server_timing_values = array(); + + $server_timing_values['wpTotal'] = microtime( true ) - $timestart; + + /* + * While values passed via Server-Timing are intended to be durations, + * any numeric value can actually be passed. + * This is a nice little trick as it allows to easily get this information in JS. + */ + $server_timing_values['wpMemoryUsage'] = memory_get_usage(); + $server_timing_values['wpDbQueries'] = $wpdb->num_queries; + + $header_values = array(); + foreach ( $server_timing_values as $slug => $value ) { + if ( is_float( $value ) ) { + $value = round( $value * 1000.0, 2 ); + } + $header_values[] = sprintf( '%1$s;dur=%2$s', $slug, $value ); + } + header( 'Server-Timing: ' . implode( ', ', $header_values ) ); + + echo $output; + }, + PHP_INT_MIN + ); + }, + PHP_INT_MAX +); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-bind/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-bind/render.php index 20f9313588046..c629e098b6e78 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-bind/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-bind/render.php @@ -93,6 +93,7 @@ <button data-testid="toggle value" data-wp-on--click="actions.toggleValue" + data-wp-bind--data-toggle-count="context.count" >Toggle</button> </div> <?php endforeach; ?> diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-bind/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-bind/view.js index 0d93e93ba1450..47d1262f37ed2 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-bind/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-bind/view.js @@ -32,6 +32,7 @@ const { state, foo } = store( 'directive-bind', { context.previousValue = context.value; context.value = previousValue; + context.count = ( context.count ?? 0 ) + 1; }, }, } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/render.php index 685e6dfc47582..a491a52161dfb 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/render.php @@ -24,4 +24,8 @@ <p data-wp-text="state.counter" data-testid="counter">0</p> </div> </div> + <div data-wp-on-document--keydown="actions.keydownHandler" data-wp-on-document--keydown--second="actions.keydownSecondHandler"> + <p data-wp-text="state.keydownHandler" data-testid="keydownHandler">no</p> + <p data-wp-text="state.keydownSecondHandler" data-testid="keydownSecondHandler">no</p> + </div> </div> diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js index c2a41832737ab..eb32d6e6e15f0 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-document/view.js @@ -25,6 +25,8 @@ const { state } = store( 'directive-on-document', { counter: 0, isVisible: true, isEventAttached: 'no', + keydownHandler: 'no', + keydownSecondHandler: 'no', }, callbacks: { keydownHandler() { @@ -39,5 +41,11 @@ const { state } = store( 'directive-on-document', { state.isEventAttached = 'no'; state.isVisible = ! state.isVisible; }, + keydownHandler: () => { + state.keydownHandler = 'yes'; + }, + keydownSecondHandler: () => { + state.keydownSecondHandler = 'yes'; + } } } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/render.php index 651b3298b64ed..c72d80e892b15 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/render.php @@ -21,4 +21,8 @@ <p data-wp-text="state.counter" data-testid="counter">0</p> </div> </div> + <div data-wp-on-window--resize="actions.resizeHandler" data-wp-on-window--resize--second="actions.resizeSecondHandler"> + <p data-wp-text="state.resizeHandler" data-testid="resizeHandler">no</p> + <p data-wp-text="state.resizeSecondHandler" data-testid="resizeSecondHandler">no</p> + </div> </div> diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js index b99f63b02517a..57c75e9c4cb63 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on-window/view.js @@ -25,6 +25,8 @@ const { state } = store( 'directive-on-window', { counter: 0, isVisible: true, isEventAttached: 'no', + resizeHandler: 'no', + resizeSecondHandler: 'no', }, callbacks: { resizeHandler() { @@ -39,5 +41,11 @@ const { state } = store( 'directive-on-window', { state.isEventAttached = 'no'; state.isVisible = ! state.isVisible; }, + resizeHandler: () => { + state.resizeHandler = 'yes'; + }, + resizeSecondHandler: () => { + state.resizeSecondHandler = 'yes'; + } } } ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-on/render.php index a1541ebe4fb8c..127d6080f8f5b 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on/render.php @@ -6,7 +6,8 @@ */ ?> -<div data-wp-interactive="directive-on"> +<?php // A wrong directive name like "data-wp-on--" should not kill the interactivity. ?> +<div data-wp-interactive="directive-on" data-wp-on--=""> <div> <p data-wp-text="state.counter" data-testid="counter">0</p> <button diff --git a/packages/e2e-tests/plugins/lightbox-allow-editing-false-enabled-false.php b/packages/e2e-tests/plugins/lightbox-allow-editing-false-enabled-false.php new file mode 100644 index 0000000000000..c5197cc719896 --- /dev/null +++ b/packages/e2e-tests/plugins/lightbox-allow-editing-false-enabled-false.php @@ -0,0 +1,26 @@ +<?php +/** + * Plugin Name: Lightbox Allow Editing False Enabled False + * Plugin URI: https://github.com/WordPress/gutenberg + * Author: Gutenberg Team + * + * @package gutenberg-lightbox-allow-editing-false-enabled-false + */ + +function filter_theme_json_theme( $theme_json ) { + $new_data = array( + 'version' => 2, + 'settings' => array( + 'blocks' => array( + 'core/image' => array( + 'lightbox' => array( + 'allowEditing' => false, + 'enabled' => false, + ), + ), + ), + ), + ); + return $theme_json->update_with( $new_data ); +} +add_filter( 'wp_theme_json_data_theme', 'filter_theme_json_theme' ); diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 65a9636a6ddb6..311279292d8f6 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -1,23 +1,9 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - /** * WordPress dependencies */ -import { - DocumentBar, - PostSavedState, - PostPreviewButton, - store as editorStore, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { useSelect } from '@wordpress/data'; -import { useViewportMatch } from '@wordpress/compose'; import { __unstableMotion as motion } from '@wordpress/components'; -import { store as preferencesStore } from '@wordpress/preferences'; -import { useState } from '@wordpress/element'; /** * Internal dependencies @@ -28,21 +14,7 @@ import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { - CollapsableBlockToolbar, - DocumentTools, - PostViewLink, - PreviewDropdown, - PinnedItems, - MoreMenu, - PostPublishButtonOrToggle, -} = unlock( editorPrivateApis ); - -const slideY = { - hidden: { y: '-50px' }, - distractionFreeInactive: { y: 0 }, - hover: { y: 0, transition: { type: 'tween', delay: 0.2 } }, -}; +const { Header: EditorHeader } = unlock( editorPrivateApis ); const slideX = { hidden: { x: '-100%' }, @@ -51,39 +23,17 @@ const slideX = { }; function Header( { setEntitiesSavedStatesCallback, initialPost } ) { - const isWideViewport = useViewportMatch( 'large' ); - const isLargeViewport = useViewportMatch( 'medium' ); - const { - isTextEditor, - hasActiveMetaboxes, - isPublishSidebarOpened, - showIconLabels, - hasHistory, - hasFixedToolbar, - } = useSelect( ( select ) => { - const { get: getPreference } = select( preferencesStore ); - const { getEditorMode } = select( editorStore ); - + const { hasActiveMetaboxes } = useSelect( ( select ) => { return { - isTextEditor: getEditorMode() === 'text', hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), - hasHistory: - !! select( editorStore ).getEditorSettings() - .onNavigateToPreviousEntityRecord, - isPublishSidebarOpened: - select( editorStore ).isPublishSidebarOpened(), - showIconLabels: getPreference( 'core', 'showIconLabels' ), - hasFixedToolbar: getPreference( 'core', 'fixedToolbar' ), }; }, [] ); - const hasTopToolbar = isLargeViewport && hasFixedToolbar; - - const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = - useState( true ); - return ( - <div className="edit-post-header"> + <EditorHeader + forceIsDirty={ hasActiveMetaboxes } + setEntitiesSavedStatesCallback={ setEntitiesSavedStatesCallback } + > <MainDashboardButton.Slot> <motion.div variants={ slideX } @@ -95,61 +45,8 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { /> </motion.div> </MainDashboardButton.Slot> - <motion.div - variants={ slideY } - transition={ { type: 'tween', delay: 0.8 } } - className="edit-post-header__toolbar" - > - <DocumentTools disableBlockTools={ isTextEditor } /> - { hasTopToolbar && ( - <CollapsableBlockToolbar - isCollapsed={ isBlockToolsCollapsed } - onToggle={ setIsBlockToolsCollapsed } - /> - ) } - <div - className={ clsx( 'edit-post-header__center', { - 'is-collapsed': - hasHistory && - ! isBlockToolsCollapsed && - hasTopToolbar, - } ) } - > - { hasHistory && <DocumentBar /> } - </div> - </motion.div> - <motion.div - variants={ slideY } - transition={ { type: 'tween', delay: 0.8 } } - className="edit-post-header__settings" - > - { ! isPublishSidebarOpened && ( - // This button isn't completely hidden by the publish sidebar. - // We can't hide the whole toolbar when the publish sidebar is open because - // we want to prevent mounting/unmounting the PostPublishButtonOrToggle DOM node. - // We track that DOM node to return focus to the PostPublishButtonOrToggle - // when the publish sidebar has been closed. - <PostSavedState forceIsDirty={ hasActiveMetaboxes } /> - ) } - <PreviewDropdown forceIsAutosaveable={ hasActiveMetaboxes } /> - <PostPreviewButton - className="edit-post-header__post-preview-button" - forceIsAutosaveable={ hasActiveMetaboxes } - /> - <PostViewLink /> - <PostPublishButtonOrToggle - forceIsDirty={ hasActiveMetaboxes } - setEntitiesSavedStatesCallback={ - setEntitiesSavedStatesCallback - } - /> - { ( isWideViewport || ! showIconLabels ) && ( - <PinnedItems.Slot scope="core" /> - ) } - <MoreMenu /> - <PostEditorMoreMenu /> - </motion.div> - </div> + <PostEditorMoreMenu /> + </EditorHeader> ); } diff --git a/packages/edit-post/src/components/header/more-menu/manage-patterns-menu-item.js b/packages/edit-post/src/components/header/more-menu/manage-patterns-menu-item.js index d12ffb88ae557..abb009cbc288e 100644 --- a/packages/edit-post/src/components/header/more-menu/manage-patterns-menu-item.js +++ b/packages/edit-post/src/components/header/more-menu/manage-patterns-menu-item.js @@ -20,7 +20,7 @@ function ManagePatternsMenuItem() { // The site editor and templates both check whether the user has // edit_theme_options capabilities. We can leverage that here and not // display the manage patterns link if the user can't access it. - return canUser( 'read', 'templates' ) ? patternsUrl : defaultUrl; + return canUser( 'create', 'templates' ) ? patternsUrl : defaultUrl; }, [] ); return ( diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss index 93c1461774cbd..53672eb09e701 100644 --- a/packages/edit-post/src/components/header/style.scss +++ b/packages/edit-post/src/components/header/style.scss @@ -1,251 +1,14 @@ -.edit-post-header { - height: $header-height; - background: $white; - display: flex; - flex-wrap: wrap; - align-items: center; - // The header should never be wider than the viewport, or buttons might be hidden. Especially relevant at high zoom levels. Related to https://core.trac.wordpress.org/ticket/47603#ticket. - max-width: 100vw; - justify-content: space-between; - - // Make toolbar sticky on larger breakpoints - @include break-zoomed-in { - flex-wrap: nowrap; - } -} - -.edit-post-header__toolbar { - display: flex; - // Allow this area to shrink to fit the toolbar buttons. - flex-shrink: 8; - // Take up the space of the toolbar so it can be justified to the left side of the toolbar. - flex-grow: 3; - // Hide the overflow so flex will limit its width. Block toolbar will allow scrolling on fixed toolbar. - overflow: hidden; - // Leave enough room for the focus ring to show. - padding: 2px 0; - align-items: center; - // Allow focus ring to be fully visible on furthest right button. - @include break-medium() { - padding-right: var(--wp-admin-border-width-focus); - } - - .table-of-contents { - display: none; - - @include break-small() { - display: block; - } - } -} - -.edit-post-header__center { - flex-grow: 1; - display: flex; - justify-content: center; - - &.is-collapsed { - display: none; - } -} - /** - * Buttons on the right side + * Show icon label overrides. */ - -.edit-post-header__settings { - display: inline-flex; - align-items: center; - flex-wrap: nowrap; - padding-right: $grid-unit-05; - - @include break-small () { - padding-right: $grid-unit-10; - } - - gap: $grid-unit-10; -} - -/** - * Show icon labels. - */ - -.show-icon-labels.interface-pinned-items, -.show-icon-labels .edit-post-header, -.edit-post-header__dropdown { - .components-button.has-icon { - width: auto; - - // Hide the button icons when labels are set to display... - svg { - display: none; - } - // ... and display labels. - &::after { - content: attr(aria-label); - } - &[aria-disabled="true"] { - background-color: transparent; - } - } - .is-tertiary { - &:active { - box-shadow: 0 0 0 1.5px var(--wp-admin-theme-color); - background-color: transparent; - } - } - // Exception for drodpdown toggle buttons. - // Exception for the fullscreen mode button. - .edit-post-fullscreen-mode-close.has-icon, - .components-button.has-icon.button-toggle { - svg { - display: block; - } - &::after { - content: none; - } - } - // Undo the width override for fullscreen mode button. +.show-icon-labels .editor-header { .edit-post-fullscreen-mode-close.has-icon { width: $header-height; - } - // Don't hide MenuItemsChoice check icons - .components-menu-items-choice .components-menu-items__item-icon.components-menu-items__item-icon { - display: block; - } - .editor-document-tools__inserter-toggle.editor-document-tools__inserter-toggle, - .interface-pinned-items .components-button { - padding-left: $grid-unit; - padding-right: $grid-unit; - - @include break-small { - padding-left: $grid-unit-15; - padding-right: $grid-unit-15; + svg { + display: block; } - } - - .editor-post-save-draft.editor-post-save-draft, - .editor-post-saved-state.editor-post-saved-state { &::after { content: none; } } } - -.show-icon-labels { - .edit-post-header__toolbar .block-editor-block-mover { - // Modified group borders. - border-left: none; - - &::before { - content: ""; - width: $border-width; - height: $grid-unit-30; - background-color: $gray-300; - margin-top: $grid-unit-05; - margin-left: $grid-unit; - } - - // Modified block movers horizontal separator. - .block-editor-block-mover__move-button-container { - &::before { - width: calc(100% - #{$grid-unit-30}); - background: $gray-300; - left: calc(50% + 1px); - } - } - } -} - -.edit-post-header__dropdown { - .components-menu-item__button.components-menu-item__button, - .components-button.editor-history__undo, - .components-button.editor-history__redo, - .table-of-contents .components-button, - .components-button.block-editor-list-view { - margin: 0; - padding: 6px 6px 6px $grid-unit-50; - width: 14.625rem; - text-align: left; - justify-content: flex-start; - } -} - -.show-icon-labels.interface-pinned-items { - padding: 6px $grid-unit-15 $grid-unit-15; - margin-top: 0; - margin-bottom: 0; - margin-left: -$grid-unit-15; - margin-right: -$grid-unit-15; - border-bottom: 1px solid $gray-400; - display: block; - - > .components-button.has-icon { - margin: 0; - padding: 6px 6px 6px $grid-unit; - width: 14.625rem; - justify-content: flex-start; - - &[aria-expanded="true"] svg { - display: block; - max-width: $grid-unit-30; - } - &[aria-expanded="false"] { - padding-left: $grid-unit-50; - } - svg { - margin-right: 8px; - } - } -} - -.edit-post-header__post-preview-button { - @include break-small { - display: none; - } -} - -.is-distraction-free { - .interface-interface-skeleton__header { - border-bottom: none; - } - - .edit-post-header { - background-color: $white; - border-bottom: 1px solid #e0e0e0; - position: absolute; - width: 100%; - - - // hide some parts - & > .edit-post-header__settings > .edit-post-header__post-preview-button { - visibility: hidden; - } - - & > .edit-post-header__toolbar .editor-document-tools__document-overview-toggle, - & > .edit-post-header__settings > .editor-preview-dropdown, - & > .edit-post-header__settings > .interface-pinned-items { - display: none; - } - - } - - // We need ! important because we override inline styles - // set by the motion component. - .interface-interface-skeleton__header:focus-within { - opacity: 1 !important; - div { - transform: translateX(0) translateZ(0) !important; - } - - } - - .components-editor-notices__dismissible { - position: absolute; - z-index: 35; - } -} - -.components-popover.more-menu-dropdown__content { - z-index: z-index(".components-popover.more-menu__content"); -} diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 74a83a6cb7561..db721b012330f 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -28,13 +28,14 @@ import { ScrollLock } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { PluginArea } from '@wordpress/plugins'; import { __, _x, sprintf } from '@wordpress/i18n'; -import { useState, useEffect, useCallback, useMemo } from '@wordpress/element'; +import { useState, useCallback, useMemo } from '@wordpress/element'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as noticesStore } from '@wordpress/notices'; import { store as preferencesStore } from '@wordpress/preferences'; import { privateApis as commandsPrivateApis } from '@wordpress/commands'; import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands'; import { privateApis as blockLibraryPrivateApis } from '@wordpress/block-library'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -45,7 +46,6 @@ import EditPostKeyboardShortcuts from '../keyboard-shortcuts'; import InitPatternModal from '../init-pattern-modal'; import BrowserURL from '../browser-url'; import Header from '../header'; -import SettingsSidebar from '../sidebar/settings-sidebar'; import MetaBoxes from '../meta-boxes'; import WelcomeGuide from '../welcome-guide'; import { store as editPostStore } from '../../store'; @@ -63,6 +63,7 @@ const { SavePublishPanels, InterfaceSkeleton, interfaceStore, + Sidebar, } = unlock( editorPrivateApis ); const { BlockKeyboardShortcuts } = unlock( blockLibraryPrivateApis ); @@ -135,13 +136,11 @@ function Layout( { initialPost } ) { useCommonCommands(); const isMobileViewport = useViewportMatch( 'medium', '<' ); - const isHugeViewport = useViewportMatch( 'huge', '>=' ); const isWideViewport = useViewportMatch( 'large' ); const isLargeViewport = useViewportMatch( 'medium' ); const { closeGeneralSidebar } = useDispatch( editPostStore ); const { createErrorNotice } = useDispatch( noticesStore ); - const { setIsInserterOpened } = useDispatch( editorStore ); const { mode, isFullscreenActive, @@ -160,6 +159,8 @@ function Layout( { initialPost } ) { documentLabel, hasHistory, hasBlockBreadcrumbs, + blockEditorMode, + isEditingTemplate, } = useSelect( ( select ) => { const { get } = select( preferencesStore ); const { getEditorSettings, getPostTypeLabel } = select( editorStore ); @@ -195,6 +196,10 @@ function Layout( { initialPost } ) { !! select( blockEditorStore ).getBlockSelectionStart(), hasHistory: !! getEditorSettings().onNavigateToPreviousEntityRecord, hasBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), + blockEditorMode: + select( blockEditorStore ).__unstableGetEditorMode(), + isEditingTemplate: + select( editorStore ).getCurrentPostType() === 'wp_template', }; }, [] ); @@ -206,18 +211,6 @@ function Layout( { initialPost } ) { const styles = useEditorStyles(); - // Inserter and Sidebars are mutually exclusive - useEffect( () => { - if ( sidebarIsOpened && ! isHugeViewport ) { - setIsInserterOpened( false ); - } - }, [ isHugeViewport, setIsInserterOpened, sidebarIsOpened ] ); - useEffect( () => { - if ( isInserterOpened && ! isHugeViewport ) { - closeGeneralSidebar(); - } - }, [ closeGeneralSidebar, isInserterOpened, isHugeViewport ] ); - // Local state for save panel. // Note 'truthy' callback implies an open panel. const [ entitiesSavedStatesCallback, setEntitiesSavedStatesCallback ] = @@ -281,6 +274,57 @@ function Layout( { initialPost } ) { ); } + const { createSuccessNotice } = useDispatch( noticesStore ); + + const onActionPerformed = useCallback( + ( actionId, items ) => { + switch ( actionId ) { + case 'move-to-trash': + { + const postType = items[ 0 ].type; + document.location.href = addQueryArgs( 'edit.php', { + post_type: postType, + } ); + } + break; + case 'duplicate-post': + { + const newItem = items[ 0 ]; + const title = + typeof newItem.title === 'string' + ? newItem.title + : newItem.title?.rendered; + createSuccessNotice( + sprintf( + // translators: %s: Title of the created post e.g: "Post 1". + __( '"%s" successfully created.' ), + title + ), + { + type: 'snackbar', + id: 'duplicate-post-action', + actions: [ + { + label: __( 'Edit' ), + onClick: () => { + const postId = newItem.id; + document.location.href = + addQueryArgs( 'post.php', { + post: postId, + action: 'edit', + } ); + }, + }, + ], + } + ); + } + break; + } + }, + [ createSuccessNotice ] + ); + return ( <> <FullscreenMode isActive={ isFullscreenActive } /> @@ -342,6 +386,7 @@ function Layout( { initialPost } ) { ! isMobileViewport && showBlockBreadcrumbs && isRichEditingEnabled && + blockEditorMode !== 'zoom-out' && mode === 'visual' && ( <div className="edit-post-layout__footer"> <BlockBreadcrumb rootLabelText={ documentLabel } /> @@ -368,7 +413,14 @@ function Layout( { initialPost } ) { <WelcomeGuide /> <InitPatternModal /> <PluginArea onError={ onPluginAreaError } /> - { ! isDistractionFree && <SettingsSidebar /> } + { ! isDistractionFree && ( + <Sidebar + onActionPerformed={ onActionPerformed } + extraPanels={ + ! isEditingTemplate && <MetaBoxes location="side" /> + } + /> + ) } </> ); } diff --git a/packages/edit-post/src/components/sidebar/post-format/style.scss b/packages/edit-post/src/components/sidebar/post-format/style.scss deleted file mode 100644 index 53d68ea219743..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-format/style.scss +++ /dev/null @@ -1,5 +0,0 @@ -.edit-post-post-format { - display: flex; - flex-direction: column; - align-items: stretch; -} diff --git a/packages/edit-post/src/components/sidebar/post-pending-status/index.js b/packages/edit-post/src/components/sidebar/post-pending-status/index.js deleted file mode 100644 index de1f02b00d746..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-pending-status/index.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * WordPress dependencies - */ -import { - PostPendingStatus as PostPendingStatusForm, - PostPendingStatusCheck, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { unlock } from '../../../lock-unlock'; - -const { PostPanelRow } = unlock( editorPrivateApis ); - -export function PostPendingStatus() { - return ( - <PostPendingStatusCheck> - <PostPanelRow> - <PostPendingStatusForm /> - </PostPanelRow> - </PostPendingStatusCheck> - ); -} - -export default PostPendingStatus; diff --git a/packages/edit-post/src/components/sidebar/post-status/index.js b/packages/edit-post/src/components/sidebar/post-status/index.js deleted file mode 100644 index 99c202463162d..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-status/index.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { - __experimentalHStack as HStack, - __experimentalVStack as VStack, - PanelBody, -} from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { - PluginPostStatusInfo, - PostAuthorPanel, - PostSchedulePanel, - PostSyncStatus, - PostURLPanel, - PostTemplatePanel, - PostFeaturedImagePanel, - store as editorStore, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import PostTrash from '../post-trash'; -import PostSticky from '../post-sticky'; -import PostSlug from '../post-slug'; -import PostFormat from '../post-format'; -import { unlock } from '../../../lock-unlock'; - -const { - PostStatus: PostStatusPanel, - PrivatePostExcerptPanel, - PostContentInformation, - PostLastEditedPanel, -} = unlock( editorPrivateApis ); - -/** - * Module Constants - */ -const PANEL_NAME = 'post-status'; - -export default function PostStatus() { - const { isOpened, isRemoved, showPostContentPanels } = useSelect( - ( select ) => { - // We use isEditorPanelRemoved to hide the panel if it was programatically removed. We do - // not use isEditorPanelEnabled since this panel should not be disabled through the UI. - const { - isEditorPanelRemoved, - isEditorPanelOpened, - getCurrentPostType, - } = select( editorStore ); - const postType = getCurrentPostType(); - return { - isRemoved: isEditorPanelRemoved( PANEL_NAME ), - isOpened: isEditorPanelOpened( PANEL_NAME ), - // Post excerpt panel is rendered in different place depending on the post type. - // So we cannot make this check inside the PostExcerpt component based on the current edited entity. - showPostContentPanels: ! [ - 'wp_template', - 'wp_template_part', - 'wp_block', - ].includes( postType ), - }; - }, - [] - ); - const { toggleEditorPanelOpened } = useDispatch( editorStore ); - - if ( isRemoved ) { - return null; - } - - return ( - <PanelBody - className="edit-post-post-status" - title={ __( 'Summary' ) } - opened={ isOpened } - onToggle={ () => toggleEditorPanelOpened( PANEL_NAME ) } - > - <PluginPostStatusInfo.Slot> - { ( fills ) => ( - <> - { showPostContentPanels && ( - <VStack - spacing={ 3 } - // TODO: this needs to be consolidated with the panel in site editor, when we unify them. - style={ { marginBlockEnd: '24px' } } - > - <PostFeaturedImagePanel - withPanelBody={ false } - /> - <PrivatePostExcerptPanel /> - <VStack spacing={ 1 }> - <PostContentInformation /> - <PostLastEditedPanel /> - </VStack> - </VStack> - ) } - <VStack - spacing={ 1 } - style={ { marginBlockEnd: '12px' } } - > - <PostStatusPanel /> - <PostSchedulePanel /> - <PostTemplatePanel /> - <PostURLPanel /> - <PostSyncStatus /> - </VStack> - <PostSticky /> - <PostFormat /> - <PostSlug /> - <PostAuthorPanel /> - { fills } - <HStack - style={ { - marginTop: '16px', - } } - > - <PostTrash /> - </HStack> - </> - ) } - </PluginPostStatusInfo.Slot> - </PanelBody> - ); -} diff --git a/packages/edit-post/src/components/sidebar/post-sticky/index.js b/packages/edit-post/src/components/sidebar/post-sticky/index.js deleted file mode 100644 index 1b31297a41294..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-sticky/index.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * WordPress dependencies - */ -import { - PostSticky as PostStickyForm, - PostStickyCheck, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { unlock } from '../../../lock-unlock'; - -const { PostPanelRow } = unlock( editorPrivateApis ); - -export function PostSticky() { - return ( - <PostStickyCheck> - <PostPanelRow> - <PostStickyForm /> - </PostPanelRow> - </PostStickyCheck> - ); -} - -export default PostSticky; diff --git a/packages/edit-post/src/components/sidebar/post-trash/index.js b/packages/edit-post/src/components/sidebar/post-trash/index.js deleted file mode 100644 index d77c7a6d82988..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-trash/index.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * WordPress dependencies - */ -import { PostTrash as PostTrashLink, PostTrashCheck } from '@wordpress/editor'; - -export default function PostTrash() { - return ( - <PostTrashCheck> - <PostTrashLink /> - </PostTrashCheck> - ); -} diff --git a/packages/edit-post/src/components/sidebar/post-visibility/index.js b/packages/edit-post/src/components/sidebar/post-visibility/index.js deleted file mode 100644 index 8bffb1b563e1e..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-visibility/index.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { Dropdown, Button } from '@wordpress/components'; -import { - PostVisibility as PostVisibilityForm, - PostVisibilityLabel, - PostVisibilityCheck, - usePostVisibilityLabel, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; -import { useMemo, useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { unlock } from '../../../lock-unlock'; - -const { PostPanelRow } = unlock( editorPrivateApis ); - -export function PostVisibility() { - // Use internal state instead of a ref to make sure that the component - // re-renders when the popover's anchor updates. - const [ popoverAnchor, setPopoverAnchor ] = useState( null ); - // Memoize popoverProps to avoid returning a new object every time. - const popoverProps = useMemo( - () => ( { - // Anchor the popover to the middle of the entire row so that it doesn't - // move around when the label changes. - anchor: popoverAnchor, - placement: 'bottom-end', - } ), - [ popoverAnchor ] - ); - - return ( - <PostVisibilityCheck - render={ ( { canEdit } ) => ( - <PostPanelRow - label={ __( 'Visibility' ) } - ref={ setPopoverAnchor } - > - { ! canEdit && ( - <span> - <PostVisibilityLabel /> - </span> - ) } - { canEdit && ( - <Dropdown - contentClassName="edit-post-post-visibility__dialog" - popoverProps={ popoverProps } - focusOnMount - renderToggle={ ( { isOpen, onToggle } ) => ( - <PostVisibilityToggle - isOpen={ isOpen } - onClick={ onToggle } - /> - ) } - renderContent={ ( { onClose } ) => ( - <PostVisibilityForm onClose={ onClose } /> - ) } - /> - ) } - </PostPanelRow> - ) } - /> - ); -} - -function PostVisibilityToggle( { isOpen, onClick } ) { - const label = usePostVisibilityLabel(); - return ( - <Button - __next40pxDefaultSize - className="edit-post-post-visibility__toggle" - variant="tertiary" - aria-expanded={ isOpen } - // translators: %s: Current post visibility. - aria-label={ sprintf( __( 'Select visibility: %s' ), label ) } - onClick={ onClick } - > - { label } - </Button> - ); -} - -export default PostVisibility; diff --git a/packages/edit-post/src/components/sidebar/post-visibility/style.scss b/packages/edit-post/src/components/sidebar/post-visibility/style.scss deleted file mode 100644 index 0dd9824e5bde7..0000000000000 --- a/packages/edit-post/src/components/sidebar/post-visibility/style.scss +++ /dev/null @@ -1,5 +0,0 @@ -.edit-post-post-visibility__dialog .editor-post-visibility { - // sidebar width - popover padding - form margin - min-width: $sidebar-width - $grid-unit-20 - $grid-unit-20; - margin: $grid-unit-10; -} diff --git a/packages/edit-post/src/components/sidebar/style.scss b/packages/edit-post/src/components/sidebar/style.scss deleted file mode 100644 index 1921c5cfd7b31..0000000000000 --- a/packages/edit-post/src/components/sidebar/style.scss +++ /dev/null @@ -1,18 +0,0 @@ -.components-panel__header.edit-post-sidebar__panel-tabs { - padding-left: 0; - padding-right: $grid-unit-20; - - .components-button.has-icon { - padding: 0; - min-width: $icon-size; - height: $icon-size; - - @include break-medium() { - display: flex; - } - } -} - -.edit-post-sidebar__panel { - margin-top: -1px; -} diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 56c6bb0404c2a..3bb50999c2a92 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -13,6 +13,7 @@ import { import { useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { store as blocksStore } from '@wordpress/blocks'; +import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies @@ -32,12 +33,13 @@ export default function VisualEditor( { styles } ) { isBlockBasedTheme, hasV3BlocksOnly, isEditingTemplate, + isZoomedOutView, } = useSelect( ( select ) => { const { isFeatureActive } = select( editPostStore ); const { getEditorSettings, getRenderingMode } = select( editorStore ); const { getBlockTypes } = select( blocksStore ); + const { __unstableGetEditorMode } = select( blockEditorStore ); const editorSettings = getEditorSettings(); - return { isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), renderingMode: getRenderingMode(), @@ -47,6 +49,7 @@ export default function VisualEditor( { styles } ) { } ), isEditingTemplate: select( editorStore ).getCurrentPostType() === 'wp_template', + isZoomedOutView: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); const hasMetaBoxes = useSelect( @@ -60,7 +63,11 @@ export default function VisualEditor( { styles } ) { // Add a constant padding for the typewritter effect. When typing at the // bottom, there needs to be room to scroll up. - if ( ! hasMetaBoxes && renderingMode === 'post-only' ) { + if ( + ! isZoomedOutView && + ! hasMetaBoxes && + renderingMode === 'post-only' + ) { paddingBottom = '40vh'; } diff --git a/packages/edit-post/src/components/visual-editor/use-padding-appender.js b/packages/edit-post/src/components/visual-editor/use-padding-appender.js index c091443495740..ff342ded90817 100644 --- a/packages/edit-post/src/components/visual-editor/use-padding-appender.js +++ b/packages/edit-post/src/components/visual-editor/use-padding-appender.js @@ -27,7 +27,7 @@ export function usePaddingAppender() { return; } - // only handle clicks under the last child + // Only handle clicks under the last child. const lastChild = node.lastElementChild; if ( ! lastChild ) { return; @@ -44,6 +44,12 @@ export function usePaddingAppender() { .select( blockEditorStore ) .getBlockOrder( '' ); const lastBlockClientId = blockOrder[ blockOrder.length - 1 ]; + + // Do nothing when only default block appender is present. + if ( ! lastBlockClientId ) { + return; + } + const lastBlock = registry .select( blockEditorStore ) .getBlock( lastBlockClientId ); diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index d780f4257f25a..be959f2d174e8 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -2,10 +2,6 @@ @import "./components/header/fullscreen-mode-close/style.scss"; @import "./components/layout/style.scss"; @import "./components/meta-boxes/meta-boxes-area/style.scss"; -@import "./components/sidebar/style.scss"; -@import "./components/sidebar/post-format/style.scss"; -@import "./components/sidebar/post-slug/style.scss"; -@import "./components/sidebar/post-visibility/style.scss"; @import "./components/text-editor/style.scss"; @import "./components/visual-editor/style.scss"; @import "./components/welcome-guide/style.scss"; @@ -39,7 +35,7 @@ body.js.block-editor-page { } // Target the editor UI excluding the visual editor contents, metaboxes and custom fields areas. -.edit-post-header, +.editor-header, .edit-post-text-editor, .editor-sidebar, .editor-post-publish-panel { diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index dfbcb4321f82f..1cf07bec6740d 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -72,9 +72,7 @@ "client-zip": "^2.4.4", "clsx": "^2.1.1", "colord": "^2.9.2", - "deepmerge": "^4.3.0", "fast-deep-equal": "^3.1.3", - "is-plain-object": "^5.0.0", "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0" }, diff --git a/packages/edit-site/src/components/app/index.js b/packages/edit-site/src/components/app/index.js index cad76b3ea1fb8..764b188acf6a5 100644 --- a/packages/edit-site/src/components/app/index.js +++ b/packages/edit-site/src/components/app/index.js @@ -2,7 +2,10 @@ * WordPress dependencies */ import { SlotFillProvider } from '@wordpress/components'; -import { UnsavedChangesWarning } from '@wordpress/editor'; +import { + UnsavedChangesWarning, + privateApis as editorPrivateApis, +} from '@wordpress/editor'; import { store as noticesStore } from '@wordpress/notices'; import { useDispatch } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; @@ -13,10 +16,10 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; * Internal dependencies */ import Layout from '../layout'; -import { GlobalStylesProvider } from '../global-styles/global-styles-provider'; import { unlock } from '../../lock-unlock'; const { RouterProvider } = unlock( routerPrivateApis ); +const { GlobalStylesProvider } = unlock( editorPrivateApis ); export default function App() { const { createErrorNotice } = useDispatch( noticesStore ); diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index 0c8b474e9a9c5..321749fa70c84 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -25,7 +25,6 @@ import { FOCUSABLE_ENTITIES, NAVIGATION_POST_TYPE, } from '../../utils/constants'; -import { computeIFrameScale } from '../../utils/math'; const { EditorCanvas: EditorCanvasRoot } = unlock( editorPrivateApis ); @@ -41,11 +40,9 @@ function EditorCanvas( { isFocusMode, templateType, canvasMode, - isZoomOutMode, currentPostIsTrashed, } = useSelect( ( select ) => { - const { getBlockCount, __unstableGetEditorMode } = - select( blockEditorStore ); + const { getBlockCount } = select( blockEditorStore ); const { getEditedPostType, getCanvasMode } = unlock( select( editSiteStore ) ); @@ -54,7 +51,6 @@ function EditorCanvas( { return { templateType: _templateType, isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', canvasMode: getCanvasMode(), hasBlocks: !! getBlockCount(), currentPostIsTrashed: @@ -139,17 +135,6 @@ function EditorCanvas( { [ settings.styles, enableResizing, canvasMode, currentPostIsTrashed ] ); - const frameSize = isZoomOutMode ? 20 : undefined; - - const scale = isZoomOutMode - ? ( contentWidth ) => - computeIFrameScale( - { width: 1000, scale: 0.55 }, - { width: 400, scale: 0.9 }, - contentWidth - ) - : undefined; - return ( <EditorCanvasRoot className={ clsx( 'edit-site-editor-canvas__block-list', { @@ -158,8 +143,6 @@ function EditorCanvas( { renderAppender={ showBlockAppender } styles={ styles } iframeProps={ { - scale, - frameSize, className: clsx( 'edit-site-visual-editor__editor-canvas', { 'is-focused': isFocused && canvasMode === 'view', } ), diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index a470308dbb700..e9a2e0f8ef529 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -21,6 +21,7 @@ .edit-site-visual-editor { position: relative; height: 100%; + overflow: hidden; display: block; background-color: $gray-300; // Centralize the editor horizontally (flex-direction is column). @@ -47,7 +48,6 @@ } .edit-site-visual-editor__editor-canvas { - border-radius: $radius-block-ui; max-height: 100%; } @@ -58,7 +58,7 @@ } } - .components-resizable-box__container { + & > .components-resizable-box__container { margin: 0 auto; } diff --git a/packages/edit-site/src/components/dataviews-actions/index.js b/packages/edit-site/src/components/dataviews-actions/index.js new file mode 100644 index 0000000000000..ed6522995d3b7 --- /dev/null +++ b/packages/edit-site/src/components/dataviews-actions/index.js @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { edit } from '@wordpress/icons'; +import { useMemo } from '@wordpress/element'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { useHistory } = unlock( routerPrivateApis ); + +export const useEditPostAction = () => { + const history = useHistory(); + return useMemo( + () => ( { + id: 'edit-post', + label: __( 'Edit' ), + isPrimary: true, + icon: edit, + isEligible( { status } ) { + return status !== 'trash'; + }, + callback( items ) { + const post = items[ 0 ]; + history.push( { + postId: post.id, + postType: post.type, + canvas: 'edit', + } ); + }, + } ), + [ history ] + ); +}; diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index dbe268fe6adb1..0f6cbc011c4ac 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -22,7 +22,6 @@ import { BlockBreadcrumb, BlockToolbar, store as blockEditorStore, - BlockInspector, } from '@wordpress/block-editor'; import { EditorKeyboardShortcutsRegister, @@ -36,14 +35,12 @@ import { __, sprintf } from '@wordpress/i18n'; import { store as coreDataStore } from '@wordpress/core-data'; import { privateApis as blockLibraryPrivateApis } from '@wordpress/block-library'; import { useState, useCallback } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import { - SidebarComplementaryAreaFills, - SidebarInspectorFill, -} from '../sidebar-edit-mode'; import CodeEditor from '../code-editor'; import Header from '../header-edit-mode'; import WelcomeGuide from '../welcome-guide'; @@ -59,6 +56,8 @@ import { POST_TYPE_LABELS, TEMPLATE_POST_TYPE } from '../../utils/constants'; import SiteEditorCanvas from '../block-editor/site-editor-canvas'; import TemplatePartConverter from '../template-part-converter'; import { useSpecificEditorSettings } from '../block-editor/use-site-editor-settings'; +import PluginTemplateSettingPanel from '../plugin-template-setting-panel'; +import GlobalStylesSidebar from '../global-styles-sidebar'; const { ExperimentalEditorProvider: EditorProvider, @@ -68,8 +67,9 @@ const { ComplementaryArea, interfaceStore, SavePublishPanels, + Sidebar, } = unlock( editorPrivateApis ); - +const { useHistory } = unlock( routerPrivateApis ); const { BlockKeyboardShortcuts } = unlock( blockLibraryPrivateApis ); const interfaceLabels = { @@ -112,14 +112,16 @@ export default function Editor( { isLoading, onClick } ) { showIconLabels, showBlockBreadcrumbs, postTypeLabel, + isEditingPage, + supportsGlobalStyles, } = useSelect( ( select ) => { const { get } = select( preferencesStore ); - const { getEditedPostContext, getCanvasMode } = unlock( + const { getEditedPostContext, getCanvasMode, isPage } = unlock( select( editSiteStore ) ); const { __unstableGetEditorMode } = select( blockEditorStore ); const { getActiveComplementaryArea } = select( interfaceStore ); - const { getEntityRecord } = select( coreDataStore ); + const { getEntityRecord, getCurrentTheme } = select( coreDataStore ); const { isInserterOpened, isListViewOpened, @@ -149,6 +151,8 @@ export default function Editor( { isLoading, onClick } ) { showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), showIconLabels: get( 'core', 'showIconLabels' ), postTypeLabel: getPostTypeLabel(), + isEditingPage: isPage(), + supportsGlobalStyles: getCurrentTheme()?.is_block_theme, }; }, [] ); @@ -207,6 +211,59 @@ export default function Editor( { isLoading, onClick } ) { [ entitiesSavedStatesCallback ] ); + const { createSuccessNotice } = useDispatch( noticesStore ); + const history = useHistory(); + const onActionPerformed = useCallback( + ( actionId, items ) => { + switch ( actionId ) { + case 'move-to-trash': + { + history.push( { + path: '/' + items[ 0 ].type, + postId: undefined, + postType: undefined, + canvas: 'view', + } ); + } + break; + case 'duplicate-post': + { + const newItem = items[ 0 ]; + const _title = + typeof newItem.title === 'string' + ? newItem.title + : newItem.title?.rendered; + createSuccessNotice( + sprintf( + // translators: %s: Title of the created post e.g: "Post 1". + __( '"%s" successfully created.' ), + _title + ), + { + type: 'snackbar', + id: 'duplicate-post-action', + actions: [ + { + label: __( 'Edit' ), + onClick: () => { + history.push( { + path: undefined, + postId: newItem.id, + postType: newItem.type, + canvas: 'edit', + } ); + }, + }, + ], + } + ); + } + break; + } + }, + [ history, createSuccessNotice ] + ); + const isReady = ! isLoading && ( ( postWithTemplate && !! contextPost && !! editedPost ) || @@ -232,7 +289,6 @@ export default function Editor( { isLoading, onClick } ) { settings={ settings } useSubRegistry={ false } > - <SidebarComplementaryAreaFills /> { isEditMode && <StartTemplateOptions /> } <InterfaceSkeleton isDistractionFree={ isDistractionFree } @@ -299,9 +355,6 @@ export default function Editor( { isLoading, onClick } ) { { showVisualEditor && ( <> <TemplatePartConverter /> - <SidebarInspectorFill> - <BlockInspector /> - </SidebarInspectorFill> { ! isLargeViewport && ( <BlockToolbar hideDragHandle /> ) } @@ -349,6 +402,15 @@ export default function Editor( { isLoading, onClick } ) { secondarySidebar: secondarySidebarLabel, } } /> + <Sidebar + onActionPerformed={ onActionPerformed } + extraPanels={ + ! isEditingPage && ( + <PluginTemplateSettingPanel.Slot /> + ) + } + /> + { supportsGlobalStyles && <GlobalStylesSidebar /> } </EditorProvider> ) } </> diff --git a/packages/edit-site/src/components/editor/style.scss b/packages/edit-site/src/components/editor/style.scss index 8803c0c59ff5a..000c64fd8ae03 100644 --- a/packages/edit-site/src/components/editor/style.scss +++ b/packages/edit-site/src/components/editor/style.scss @@ -6,10 +6,6 @@ &.is-loading { opacity: 0; } - - .interface-interface-skeleton__header { - border: 0; - } } .edit-site-editor__toggle-save-panel { diff --git a/packages/edit-site/src/components/sidebar-edit-mode/default-sidebar.js b/packages/edit-site/src/components/global-styles-sidebar/default-sidebar.js similarity index 100% rename from packages/edit-site/src/components/sidebar-edit-mode/default-sidebar.js rename to packages/edit-site/src/components/global-styles-sidebar/default-sidebar.js diff --git a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js b/packages/edit-site/src/components/global-styles-sidebar/index.js similarity index 97% rename from packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js rename to packages/edit-site/src/components/global-styles-sidebar/index.js index f339b34784741..436762d6bcf94 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js +++ b/packages/edit-site/src/components/global-styles-sidebar/index.js @@ -21,12 +21,12 @@ import { /** * Internal dependencies */ -import DefaultSidebar from './default-sidebar'; import { GlobalStylesUI } from '../global-styles'; import { store as editSiteStore } from '../../store'; import { GlobalStylesMenuSlot } from '../global-styles/ui'; import { unlock } from '../../lock-unlock'; import { store as coreStore } from '@wordpress/core-data'; +import DefaultSidebar from './default-sidebar'; const { interfaceStore } = unlock( editorPrivateApis ); @@ -136,7 +136,10 @@ export default function GlobalStylesSidebar() { closeLabel={ __( 'Close Styles' ) } panelClassName="edit-site-global-styles-sidebar__panel" header={ - <Flex className="edit-site-global-styles-sidebar__header"> + <Flex + className="edit-site-global-styles-sidebar__header" + gap={ 1 } + > <FlexBlock style={ { minWidth: 'min-content' } }> <h2 className="edit-site-global-styles-sidebar__header-title"> { __( 'Styles' ) } @@ -151,6 +154,7 @@ export default function GlobalStylesSidebar() { } disabled={ shouldClearCanvasContainerView } onClick={ toggleStyleBook } + size="compact" /> </FlexItem> <FlexItem> @@ -162,6 +166,7 @@ export default function GlobalStylesSidebar() { isPressed={ isRevisionsOpened || isRevisionsStyleBookOpened } + size="compact" /> </FlexItem> <GlobalStylesMenuSlot /> diff --git a/packages/edit-site/src/components/sidebar-edit-mode/style.scss b/packages/edit-site/src/components/global-styles-sidebar/style.scss similarity index 88% rename from packages/edit-site/src/components/sidebar-edit-mode/style.scss rename to packages/edit-site/src/components/global-styles-sidebar/style.scss index 186908c88a54b..1f631eb24057c 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/style.scss +++ b/packages/edit-site/src/components/global-styles-sidebar/style.scss @@ -38,14 +38,6 @@ margin: 0; } -.edit-site-global-styles-sidebar .interface-complementary-area-header .components-button.has-icon { - margin-left: 0; -} - -.edit-site-global-styles-sidebar__reset-button.components-button { - margin-left: auto; -} - .edit-site-global-styles-sidebar .components-navigation__menu-title-heading { font-size: $default-font-size * 1.2; font-weight: 500; @@ -104,6 +96,3 @@ } } -.edit-site-sidebar__panel { - margin-top: -1px; -} diff --git a/packages/edit-site/src/components/global-styles/background-panel.js b/packages/edit-site/src/components/global-styles/background-panel.js index 2addf109873aa..65dd9738b2b4f 100644 --- a/packages/edit-site/src/components/global-styles/background-panel.js +++ b/packages/edit-site/src/components/global-styles/background-panel.js @@ -20,6 +20,21 @@ const { BackgroundPanel: StylesBackgroundPanel, } = unlock( blockEditorPrivateApis ); +/** + * Checks if there is a current value in the background image block support + * attributes. + * + * @param {Object} style Style attribute. + * @return {boolean} Whether the block has a background image value set. + */ +export function hasBackgroundImageValue( style ) { + return ( + !! style?.background?.backgroundImage?.id || + !! style?.background?.backgroundImage?.url || + typeof style?.background?.backgroundImage === 'string' + ); +} + export default function BackgroundPanel() { const [ style ] = useGlobalStyle( '', undefined, 'user', { shouldDecodeEncode: false, @@ -32,8 +47,8 @@ export default function BackgroundPanel() { const defaultControls = { backgroundImage: true, backgroundSize: - !! style?.background?.backgroundImage && - !! inheritedStyle?.background?.backgroundImage, + hasBackgroundImageValue( style ) || + hasBackgroundImageValue( inheritedStyle ), }; return ( diff --git a/packages/edit-site/src/components/global-styles/color-palette-panel.js b/packages/edit-site/src/components/global-styles/color-palette-panel.js index 68329b3147ca9..e3e322c57f6d4 100644 --- a/packages/edit-site/src/components/global-styles/color-palette-panel.js +++ b/packages/edit-site/src/components/global-styles/color-palette-panel.js @@ -52,7 +52,7 @@ export default function ColorPalettePanel( { name } ) { return ( <VStack className="edit-site-global-styles-color-palette-panel" - spacing={ 10 } + spacing={ 8 } > { !! themeColors && !! themeColors.length && ( <PaletteEdit @@ -83,9 +83,6 @@ export default function ColorPalettePanel( { name } ) { onChange={ setCustomColors } paletteLabel={ __( 'Custom' ) } paletteLabelHeadingLevel={ 3 } - emptyMessage={ __( - 'Custom colors are empty! Add some colors to create your own color palette.' - ) } slugPrefix="custom-" popoverProps={ popoverProps } /> diff --git a/packages/edit-site/src/components/global-styles/gradients-palette-panel.js b/packages/edit-site/src/components/global-styles/gradients-palette-panel.js index 9ebc66c87c347..00eb8db83578d 100644 --- a/packages/edit-site/src/components/global-styles/gradients-palette-panel.js +++ b/packages/edit-site/src/components/global-styles/gradients-palette-panel.js @@ -71,7 +71,7 @@ export default function GradientPalettePanel( { name } ) { return ( <VStack className="edit-site-global-styles-gradient-palette-panel" - spacing={ 10 } + spacing={ 8 } > { !! themeGradients && !! themeGradients.length && ( <PaletteEdit @@ -102,9 +102,6 @@ export default function GradientPalettePanel( { name } ) { onChange={ setCustomGradients } paletteLabel={ __( 'Custom' ) } paletteLabelLevel={ 3 } - emptyMessage={ __( - 'Custom gradients are empty! Add some gradients to create your own palette.' - ) } slugPrefix="custom-" popoverProps={ popoverProps } /> diff --git a/packages/edit-site/src/components/global-styles/hooks.js b/packages/edit-site/src/components/global-styles/hooks.js index 6ecb1de03a144..50032786e3994 100644 --- a/packages/edit-site/src/components/global-styles/hooks.js +++ b/packages/edit-site/src/components/global-styles/hooks.js @@ -9,18 +9,19 @@ import a11yPlugin from 'colord/plugins/a11y'; */ import { store as blocksStore } from '@wordpress/blocks'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { useContext } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { mergeBaseAndUserConfigs } from './global-styles-provider'; import { useCurrentMergeThemeStyleVariationsWithUserConfig } from '../../hooks/use-theme-style-variations/use-theme-style-variations-by-property'; import { getFontFamilies } from './utils'; import { unlock } from '../../lock-unlock'; import { useSelect } from '@wordpress/data'; +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); const { useGlobalSetting, useGlobalStyle, GlobalStylesContext } = unlock( blockEditorPrivateApis ); diff --git a/packages/edit-site/src/components/global-styles/stories/index.story.js b/packages/edit-site/src/components/global-styles/stories/index.story.js index f04387295c458..66cf468d5e224 100644 --- a/packages/edit-site/src/components/global-styles/stories/index.story.js +++ b/packages/edit-site/src/components/global-styles/stories/index.story.js @@ -2,15 +2,16 @@ * WordPress dependencies */ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { useMemo, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { mergeBaseAndUserConfigs } from '../global-styles-provider'; import { default as GlobalStylesUIComponent } from '../ui'; import { unlock } from '../../../lock-unlock'; +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); const { GlobalStylesContext, ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); diff --git a/packages/edit-site/src/components/global-styles/typography-example.js b/packages/edit-site/src/components/global-styles/typography-example.js index dfb8920fb5ea2..a491ca57bf5be 100644 --- a/packages/edit-site/src/components/global-styles/typography-example.js +++ b/packages/edit-site/src/components/global-styles/typography-example.js @@ -5,16 +5,17 @@ import { useContext } from '@wordpress/element'; import { __unstableMotion as motion } from '@wordpress/components'; import { _x } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies */ -import { mergeBaseAndUserConfigs } from './global-styles-provider'; import { unlock } from '../../lock-unlock'; import { getFamilyPreviewStyle } from './font-library-modal/utils/preview-styles'; import { getFontFamilies } from './utils'; const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); export default function PreviewTypography( { fontSize, variation } ) { const { base } = useContext( GlobalStylesContext ); diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index b50e09550f707..09c1a8b341f8f 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -76,7 +76,11 @@ function GlobalStylesActionMenu() { return ( <GlobalStylesMenuFill> - <DropdownMenu icon={ moreVertical } label={ __( 'More' ) }> + <DropdownMenu + icon={ moreVertical } + label={ __( 'More' ) } + toggleProps={ { size: 'compact' } } + > { ( { onClose } ) => ( <> <MenuGroup> diff --git a/packages/edit-site/src/components/global-styles/variations/variation.js b/packages/edit-site/src/components/global-styles/variations/variation.js index 877a0095e22ac..26f5235dece6f 100644 --- a/packages/edit-site/src/components/global-styles/variations/variation.js +++ b/packages/edit-site/src/components/global-styles/variations/variation.js @@ -10,13 +10,14 @@ import { useMemo, useContext, useState } from '@wordpress/element'; import { ENTER } from '@wordpress/keycodes'; import { __, sprintf } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies */ -import { mergeBaseAndUserConfigs } from '../global-styles-provider'; import { unlock } from '../../../lock-unlock'; +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( blockEditorPrivateApis ); diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js deleted file mode 100644 index cd7720a1a34f9..0000000000000 --- a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * WordPress dependencies - */ -import { useViewportMatch } from '@wordpress/compose'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; -import { chevronUpDown } from '@wordpress/icons'; -import { Button, ToolbarItem } from '@wordpress/components'; -import { - store as editorStore, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { unlock } from '../../../lock-unlock'; - -const { DocumentTools: EditorDocumentTools } = unlock( editorPrivateApis ); - -export default function DocumentTools( { - blockEditorMode, - hasFixedToolbar, - isDistractionFree, -} ) { - const { isVisualMode } = useSelect( ( select ) => { - const { getEditorMode } = select( editorStore ); - - return { - isVisualMode: getEditorMode() === 'visual', - }; - }, [] ); - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); - const { setDeviceType } = useDispatch( editorStore ); - const isLargeViewport = useViewportMatch( 'medium' ); - const isZoomedOutViewExperimentEnabled = - window?.__experimentalEnableZoomedOutView && isVisualMode; - const isZoomedOutView = blockEditorMode === 'zoom-out'; - - return ( - <EditorDocumentTools - disableBlockTools={ ! isVisualMode } - listViewLabel={ __( 'List View' ) } - > - { isZoomedOutViewExperimentEnabled && - isLargeViewport && - ! isDistractionFree && - ! hasFixedToolbar && ( - <ToolbarItem - as={ Button } - className="edit-site-header-edit-mode__zoom-out-view-toggle" - icon={ chevronUpDown } - isPressed={ isZoomedOutView } - /* translators: button label text should, if possible, be under 16 characters. */ - label={ __( 'Zoom-out View' ) } - onClick={ () => { - setDeviceType( 'Desktop' ); - __unstableSetEditorMode( - isZoomedOutView ? 'edit' : 'zoom-out' - ); - } } - size="compact" - /> - ) } - </EditorDocumentTools> - ); -} diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index 4ae9db1fea80c..fcd4ea1b38802 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -1,190 +1,51 @@ -/** - * External dependencies - */ -import clsx from 'clsx'; - /** * WordPress dependencies */ -import { useViewportMatch, useReducedMotion } from '@wordpress/compose'; -import { store as blockEditorStore } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; -import { __unstableMotion as motion } from '@wordpress/components'; -import { store as preferencesStore } from '@wordpress/preferences'; -import { - DocumentBar, - PostSavedState, - store as editorStore, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; /** * Internal dependencies */ -import SiteEditorMoreMenuItems from './more-menu'; +import SiteEditorMoreMenu from './more-menu'; +import { unlock } from '../../lock-unlock'; import SaveButton from '../save-button'; -import DocumentTools from './document-tools'; -import { store as editSiteStore } from '../../store'; +import { isPreviewingTheme } from '../../utils/is-previewing-theme'; import { getEditorCanvasContainerTitle, useHasEditorCanvasContainer, } from '../editor-canvas-container'; -import { unlock } from '../../lock-unlock'; -import { FOCUSABLE_ENTITIES } from '../../utils/constants'; -import { isPreviewingTheme } from '../../utils/is-previewing-theme'; - -const { - CollapsableBlockToolbar, - MoreMenu, - PostViewLink, - PreviewDropdown, - PinnedItems, - PostPublishButtonOrToggle, -} = unlock( editorPrivateApis ); +import { store as editSiteStore } from '../../store'; -export default function HeaderEditMode( { setEntitiesSavedStatesCallback } ) { - const { - templateType, - isDistractionFree, - blockEditorMode, - showIconLabels, - editorCanvasView, - isFixedToolbar, - isPublishSidebarOpened, - } = useSelect( ( select ) => { - const { getEditedPostType } = select( editSiteStore ); - const { __unstableGetEditorMode } = select( blockEditorStore ); - const { get: getPreference } = select( preferencesStore ); - const { getDeviceType } = select( editorStore ); +const { Header: EditorHeader } = unlock( editorPrivateApis ); +function Header( { setEntitiesSavedStatesCallback } ) { + const _isPreviewingTheme = isPreviewingTheme(); + const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer(); + const { editorCanvasView } = useSelect( ( select ) => { return { - deviceType: getDeviceType(), - templateType: getEditedPostType(), - blockEditorMode: __unstableGetEditorMode(), - showIconLabels: getPreference( 'core', 'showIconLabels' ), editorCanvasView: unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), - isDistractionFree: getPreference( 'core', 'distractionFree' ), - isFixedToolbar: getPreference( 'core', 'fixedToolbar' ), - isPublishSidebarOpened: - select( editorStore ).isPublishSidebarOpened(), }; }, [] ); - const isLargeViewport = useViewportMatch( 'medium' ); - const showTopToolbar = - isLargeViewport && isFixedToolbar && blockEditorMode !== 'zoom-out'; - const disableMotion = useReducedMotion(); - - const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer(); - - const isFocusMode = FOCUSABLE_ENTITIES.includes( templateType ); - - const isZoomedOutView = blockEditorMode === 'zoom-out'; - - const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = - useState( true ); - - const toolbarVariants = { - isDistractionFree: { y: '-50px' }, - isDistractionFreeHovering: { y: 0 }, - view: { y: 0 }, - edit: { y: 0 }, - }; - - const toolbarTransition = { - type: 'tween', - duration: disableMotion ? 0 : 0.2, - ease: 'easeOut', - }; - - const _isPreviewingTheme = isPreviewingTheme(); return ( - <div - className={ clsx( 'edit-site-header-edit-mode', { - 'show-icon-labels': showIconLabels, - 'show-block-toolbar': ! isBlockToolsCollapsed && showTopToolbar, - } ) } + <EditorHeader + setEntitiesSavedStatesCallback={ setEntitiesSavedStatesCallback } + customSaveButton={ + _isPreviewingTheme && <SaveButton size="compact" /> + } + forceDisableBlockTools={ ! hasDefaultEditorCanvasView } + title={ + ! hasDefaultEditorCanvasView + ? getEditorCanvasContainerTitle( editorCanvasView ) + : undefined + } > - { hasDefaultEditorCanvasView && ( - <motion.div - className="edit-site-header-edit-mode__start" - variants={ toolbarVariants } - transition={ toolbarTransition } - > - <DocumentTools - blockEditorMode={ blockEditorMode } - isDistractionFree={ isDistractionFree } - /> - { showTopToolbar && ( - <CollapsableBlockToolbar - isCollapsed={ isBlockToolsCollapsed } - onToggle={ setIsBlockToolsCollapsed } - /> - ) } - </motion.div> - ) } - - { ! isDistractionFree && ( - <div className="edit-site-header-edit-mode__center"> - { ! hasDefaultEditorCanvasView ? ( - getEditorCanvasContainerTitle( editorCanvasView ) - ) : ( - <DocumentBar /> - ) } - </div> - ) } - - <div className="edit-site-header-edit-mode__end"> - <motion.div - className="edit-site-header-edit-mode__actions" - variants={ toolbarVariants } - transition={ toolbarTransition } - > - { isLargeViewport && ( - <div - className={ clsx( - 'edit-site-header-edit-mode__preview-options', - { 'is-zoomed-out': isZoomedOutView } - ) } - > - <PreviewDropdown - disabled={ - isFocusMode || ! hasDefaultEditorCanvasView - } - /> - </div> - ) } - <PostViewLink /> - { - // TODO: For now we conditionally render the Save/Publish buttons based on - // some specific site editor extra handling. Examples are when we're previewing - // a theme, handling of global styles changes or when we're in 'view' mode, - // which opens the save panel in a Modal. - } - { ! _isPreviewingTheme && ! isPublishSidebarOpened && ( - // This button isn't completely hidden by the publish sidebar. - // We can't hide the whole toolbar when the publish sidebar is open because - // we want to prevent mounting/unmounting the PostPublishButtonOrToggle DOM node. - // We track that DOM node to return focus to the PostPublishButtonOrToggle - // when the publish sidebar has been closed. - <PostSavedState /> - ) } - { ! _isPreviewingTheme && ( - <PostPublishButtonOrToggle - setEntitiesSavedStatesCallback={ - setEntitiesSavedStatesCallback - } - /> - ) } - { _isPreviewingTheme && <SaveButton size="compact" /> } - { ! isDistractionFree && <PinnedItems.Slot scope="core" /> } - <MoreMenu /> - <SiteEditorMoreMenuItems /> - </motion.div> - </div> - </div> + <SiteEditorMoreMenu /> + </EditorHeader> ); } + +export default Header; diff --git a/packages/edit-site/src/components/header-edit-mode/style.scss b/packages/edit-site/src/components/header-edit-mode/style.scss index 5963a1c815141..69b1e9dff3849 100644 --- a/packages/edit-site/src/components/header-edit-mode/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/style.scss @@ -1,201 +1,3 @@ -.edit-site-header-edit-mode { - height: $header-height; - align-items: center; - background-color: $white; - color: $gray-900; - display: flex; - box-sizing: border-box; - width: 100%; - justify-content: space-between; - border-bottom: $border-width solid $gray-200; +.editor-header { padding-left: $header-height; - - // When top toolbar is engaged and should expand fully. - &.show-block-toolbar { - - .edit-site-header-edit-mode__start, - .edit-site-header-edit-mode__end { - flex-basis: auto; - } - - .edit-site-header-edit-mode__center { - display: none; - } - } - - .edit-site-header-edit-mode__start { - display: flex; - border: none; - align-items: center; - flex-grow: 1; - flex-shrink: 2; - // Take up the full height of the header so the border focus - // is visible on toolbar buttons. - height: 100%; - // Allow focus ring to be fully visible on furthest right button. - @include break-medium() { - padding-right: var(--wp-admin-border-width-focus); - // Account for the site hub, which is 60x60px. - flex-basis: calc(37.5% - 60px); - // We need this to be overflow hidden so the block toolbar can - // overflow scroll. If the overflow is visible, flexbox allows - // the toolbar to grow outside of the allowed container space. - overflow: hidden; - } - } - - .edit-site-header-edit-mode__end { - display: flex; - justify-content: flex-end; - height: 100%; - flex-grow: 1; - flex-shrink: 1; - - @include break-medium() { - flex-basis: 37.5%; - } - } - - .edit-site-header-edit-mode__center { - align-items: center; - display: flex; - flex-basis: 100%; - flex-grow: 1; - flex-shrink: 2; - height: 100%; - justify-content: center; - - // Flex items will, by default, refuse to shrink below a minimum - // intrinsic width. In order to shrink this flexbox item, and - // subsequently truncate child text, we set an explicit min-width. - // See https://dev.w3.org/csswg/css-flexbox/#min-size-auto - min-width: 0; - - @include break-medium() { - flex-basis: 25%; - } - } - -} - -.edit-site-header-edit-mode__toolbar { - align-items: center; - display: flex; - gap: $grid-unit-10; - padding-left: $grid-unit-20; - - @include break-medium() { - padding-left: $grid-unit-50 * 0.5; - } - - @include break-wide() { - padding-right: $grid-unit-10; - } - - .edit-site-header-edit-mode__inserter-toggle { - svg { - transition: transform cubic-bezier(0.165, 0.84, 0.44, 1) 0.2s; - @include reduce-motion("transition"); - } - - &.is-pressed { - svg { - transform: rotate(45deg); - } - } - } -} - -/** - * Buttons on the right side - */ - -.edit-site-header-edit-mode__actions { - display: inline-flex; - align-items: center; - flex-wrap: nowrap; - // Ensure actions do not press against .edit-site-header-edit-mode__center. - padding-left: $grid-unit-10; - padding-right: $grid-unit-10; - gap: $grid-unit-10; -} - -.edit-site-header-edit-mode__preview-options { - opacity: 1; - transition: opacity 0.3s; - - &.is-zoomed-out { - opacity: 0; - } -} - -// Button text label styles - -.edit-site-header-edit-mode.show-icon-labels { - .components-button.has-icon { - width: auto; - - // Hide the button icons when labels are set to display... - svg { - display: none; - } - // ... and display labels. - &::after { - content: attr(aria-label); - } - &[aria-disabled="true"] { - background-color: transparent; - } - } - .is-tertiary { - &:active { - box-shadow: 0 0 0 1.5px var(--wp-admin-theme-color); - background-color: transparent; - } - } - // Some margins and padding have to be adjusted so the buttons can still fit on smaller screens. - .edit-site-save-button__button { - padding-left: 6px; - padding-right: 6px; - } - - // The template details toggle has a custom label, different from its aria-label, so we don't want to display both. - .edit-site-document-actions__get-info.edit-site-document-actions__get-info.edit-site-document-actions__get-info { - &::after { - content: none; - } - } - - .edit-site-header-edit-mode__inserter-toggle.edit-site-header-edit-mode__inserter-toggle, - .edit-site-document-actions__get-info.edit-site-document-actions__get-info.edit-site-document-actions__get-info { - height: 36px; - padding: 0 $grid-unit-10; - } - - .block-editor-block-mover { - // Modified group borders. - border-left: none; - - &::before { - content: ""; - width: $border-width; - height: $grid-unit-30; - background-color: $gray-300; - margin-top: $grid-unit-05; - margin-left: $grid-unit; - } - - // Modified block movers horizontal separator. - .block-editor-block-mover__move-button-container { - &::before { - width: calc(100% - #{$grid-unit-30}); - background: $gray-300; - left: calc(50% + 1px); - } - } - } -} - -.components-popover.more-menu-dropdown__content { - z-index: z-index(".components-popover.more-menu__content"); } diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 9a3c3100c4be7..d8b0d85e484ed 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -72,7 +72,6 @@ export default function Layout() { const { isDistractionFree, - isZoomOutMode, hasFixedToolbar, hasBlockSelected, canvasMode, @@ -104,9 +103,6 @@ export default function Layout() { 'core', 'showBlockBreadcrumbs' ), - isZoomOutMode: - select( blockEditorStore ).__unstableGetEditorMode() === - 'zoom-out', hasBlockSelected: select( blockEditorStore ).getBlockSelectionStart(), }; @@ -186,7 +182,6 @@ export default function Layout() { 'is-full-canvas': canvasMode === 'edit', 'has-fixed-toolbar': hasFixedToolbar, 'is-block-toolbar-visible': hasBlockSelected, - 'is-zoom-out': isZoomOutMode, 'has-block-breadcrumbs': hasBlockBreadcrumbs && ! isDistractionFree && diff --git a/packages/edit-site/src/components/layout/router.js b/packages/edit-site/src/components/layout/router.js index fc19ca9242eac..20d5b463852ff 100644 --- a/packages/edit-site/src/components/layout/router.js +++ b/packages/edit-site/src/components/layout/router.js @@ -31,7 +31,7 @@ export default function useLayoutAreas() { const isSiteEditorLoading = useIsSiteEditorLoading(); const history = useHistory(); const { params } = useLocation(); - const { postType, postId, path, layout, isCustom, canvas } = params ?? {}; + const { postType, postId, path, layout, isCustom, canvas } = params; // Note: Since "sidebar" is not yet supported here, // returning undefined from "mobile" means show the sidebar. @@ -45,6 +45,7 @@ export default function useLayoutAreas() { sidebar: ( <SidebarNavigationScreen title={ __( 'Manage pages' ) } + backPath={ {} } content={ <DataViewsSidebarContent /> } /> ), @@ -78,13 +79,33 @@ export default function useLayoutAreas() { if ( postType && postId ) { let sidebar; if ( postType === 'wp_template_part' || postType === 'wp_block' ) { - sidebar = <SidebarNavigationScreenPattern />; + sidebar = ( + <SidebarNavigationScreenPattern + backPath={ { + path: '/patterns', + categoryId: params.categoryId, + categoryType: params.categoryType, + } } + /> + ); } else if ( postType === 'wp_template' ) { - sidebar = <SidebarNavigationScreenTemplate />; + sidebar = ( + <SidebarNavigationScreenTemplate + backPath={ { path: '/wp_template' } } + /> + ); } else if ( postType === 'page' ) { - sidebar = <SidebarNavigationScreenPage />; + sidebar = ( + <SidebarNavigationScreenPage + backPath={ { path: '/page', postId } } + /> + ); } else { - sidebar = <SidebarNavigationScreenNavigationMenu />; + sidebar = ( + <SidebarNavigationScreenNavigationMenu + backPath={ { path: '/navigation' } } + /> + ); } return { key: 'page', @@ -104,7 +125,9 @@ export default function useLayoutAreas() { return { key: 'templates-list', areas: { - sidebar: <SidebarNavigationScreenTemplatesBrowse />, + sidebar: ( + <SidebarNavigationScreenTemplatesBrowse backPath={ {} } /> + ), content: <PageTemplates />, preview: isListLayout && ( <Editor isLoading={ isSiteEditorLoading } /> @@ -125,7 +148,7 @@ export default function useLayoutAreas() { return { key: 'patterns', areas: { - sidebar: <SidebarNavigationScreenPatterns />, + sidebar: <SidebarNavigationScreenPatterns backPath={ {} } />, content: <PagePatterns />, mobile: <PagePatterns />, }, @@ -137,7 +160,9 @@ export default function useLayoutAreas() { return { key: 'styles', areas: { - sidebar: <SidebarNavigationScreenGlobalStyles />, + sidebar: ( + <SidebarNavigationScreenGlobalStyles backPath={ {} } /> + ), preview: <Editor isLoading={ isSiteEditorLoading } />, mobile: canvas === 'edit' && ( <Editor isLoading={ isSiteEditorLoading } /> @@ -152,7 +177,11 @@ export default function useLayoutAreas() { return { key: 'navigation', areas: { - sidebar: <SidebarNavigationScreenNavigationMenu />, + sidebar: ( + <SidebarNavigationScreenNavigationMenu + backPath={ { path: '/navigation' } } + /> + ), preview: <Editor isLoading={ isSiteEditorLoading } />, mobile: canvas === 'edit' && ( <Editor isLoading={ isSiteEditorLoading } /> @@ -163,7 +192,9 @@ export default function useLayoutAreas() { return { key: 'navigation', areas: { - sidebar: <SidebarNavigationScreenNavigationMenus />, + sidebar: ( + <SidebarNavigationScreenNavigationMenus backPath={ {} } /> + ), preview: <Editor isLoading={ isSiteEditorLoading } />, mobile: canvas === 'edit' && ( <Editor isLoading={ isSiteEditorLoading } /> diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 00b67f00f6f23..55a071caacac4 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -165,8 +165,9 @@ position: relative; color: $white; border-radius: 0; - height: $header-height; + height: $header-height + $border-width; width: $header-height; + margin-bottom: - $border-width; overflow: hidden; padding: 0; display: flex; diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 8ac080c0ac97f..1215320f4df9f 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -32,11 +32,10 @@ import { import AddNewPageModal from '../add-new-page'; import Media from '../media'; import { unlock } from '../../lock-unlock'; +import { useEditPostAction } from '../dataviews-actions'; const { usePostActions } = unlock( editorPrivateApis ); - const { useLocation, useHistory } = unlock( routerPrivateApis ); - const EMPTY_ARRAY = []; function useView( postType ) { @@ -188,29 +187,6 @@ function FeaturedImage( { item, viewType } ) { ); } -let PAGE_ACTIONS = [ - 'edit-post', - 'view-post', - 'restore', - 'permanently-delete', - 'view-post-revisions', - 'rename-post', - 'move-to-trash', -]; - -if ( process.env.IS_GUTENBERG_PLUGIN ) { - PAGE_ACTIONS = [ - 'edit-post', - 'view-post', - 'restore', - 'permanently-delete', - 'view-post-revisions', - 'duplicate-post', - 'rename-post', - 'move-to-trash', - ]; -} - export default function PagePages() { const postType = 'page'; const [ view, setView ] = useView( postType ); @@ -360,20 +336,14 @@ export default function PagePages() { ], [ authors, view.type ] ); - const onActionPerformed = useCallback( - ( actionId, items ) => { - if ( actionId === 'edit-post' ) { - const post = items[ 0 ]; - history.push( { - postId: post.id, - postType: post.type, - canvas: 'edit', - } ); - } - }, - [ history ] + + const postTypeActions = usePostActions( 'page' ); + const editAction = useEditPostAction(); + const actions = useMemo( + () => [ editAction, ...postTypeActions ], + [ postTypeActions, editAction ] ); - const actions = usePostActions( onActionPerformed, PAGE_ACTIONS ); + const onChangeView = useCallback( ( newView ) => { if ( newView.type !== view.type ) { diff --git a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js index 12a2b99e7dbcb..afa69e9752c5b 100644 --- a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js +++ b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js @@ -11,14 +11,11 @@ import { downloadBlob } from '@wordpress/blob'; import { __, _x, sprintf } from '@wordpress/i18n'; import { Button, - TextControl, __experimentalHStack as HStack, __experimentalVStack as VStack, __experimentalText as Text, } from '@wordpress/components'; -import { store as coreStore } from '@wordpress/core-data'; import { useDispatch } from '@wordpress/data'; -import { useState } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { decodeEntities } from '@wordpress/html-entities'; import { store as reusableBlocksStore } from '@wordpress/reusable-blocks'; @@ -91,92 +88,6 @@ export const exportJSONaction = { }, }; -export const renameAction = { - id: 'rename-pattern', - label: __( 'Rename' ), - isEligible: ( item ) => { - const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; - const isUserPattern = item.type === PATTERN_TYPES.user; - const isCustomPattern = - isUserPattern || ( isTemplatePart && item.isCustom ); - const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file; - return isCustomPattern && ! hasThemeFile; - }, - RenderModal: ( { items, closeModal } ) => { - const [ item ] = items; - const [ title, setTitle ] = useState( () => item.title ); - const { editEntityRecord, saveEditedEntityRecord } = - useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - async function onRename( event ) { - event.preventDefault(); - try { - await editEntityRecord( 'postType', item.type, item.id, { - title, - } ); - // Update state before saving rerenders the list. - setTitle( '' ); - closeModal(); - // Persist edited entity. - await saveEditedEntityRecord( 'postType', item.type, item.id, { - throwOnError: true, - } ); - createSuccessNotice( - item.type === TEMPLATE_PART_POST_TYPE - ? __( 'Template part renamed.' ) - : __( 'Pattern renamed.' ), - { type: 'snackbar' } - ); - } catch ( error ) { - const fallbackErrorMessage = - item.type === TEMPLATE_PART_POST_TYPE - ? __( - 'An error occurred while renaming the template part.' - ) - : __( 'An error occurred while renaming the pattern.' ); - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : fallbackErrorMessage; - createErrorNotice( errorMessage, { type: 'snackbar' } ); - } - } - return ( - <form onSubmit={ onRename }> - <VStack spacing="5"> - <TextControl - __nextHasNoMarginBottom - __next40pxDefaultSize - label={ __( 'Name' ) } - value={ title } - onChange={ setTitle } - required - /> - <HStack justify="right"> - <Button - __next40pxDefaultSize - variant="tertiary" - onClick={ () => { - closeModal(); - } } - > - { __( 'Cancel' ) } - </Button> - <Button - __next40pxDefaultSize - variant="primary" - type="submit" - > - { __( 'Save' ) } - </Button> - </HStack> - </VStack> - </form> - ); - }, -}; - const canDeleteOrReset = ( item ) => { const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; const isUserPattern = item.type === PATTERN_TYPES.user; diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index bf3419ad768bc..724f60ba39103 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -48,7 +48,6 @@ import { } from '../../utils/constants'; import { exportJSONaction, - renameAction, resetAction, deleteAction, duplicatePatternAction, @@ -60,12 +59,13 @@ import usePatterns from './use-patterns'; import PatternsHeader from './header'; import { useLink } from '../routes/link'; import { useAddedBy } from '../page-templates/hooks'; +import { useEditPostAction } from '../dataviews-actions'; const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( blockEditorPrivateApis ); const { usePostActions } = unlock( editorPrivateApis ); -const { useHistory, useLocation } = unlock( routerPrivateApis ); +const { useLocation } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; const defaultConfigPerViewType = { @@ -375,45 +375,29 @@ export default function DataviewsPatterns() { return filterSortAndPaginate( patterns, viewWithoutFilters, fields ); }, [ patterns, view, fields, type ] ); - const history = useHistory(); - const onActionPerformed = useCallback( - ( actionId, items ) => { - if ( actionId === 'edit-post' ) { - const post = items[ 0 ]; - history.push( { - postId: post.id, - postType: post.type, - categoryId, - categoryType: type, - canvas: 'edit', - } ); - } - }, - [ history, categoryId, type ] - ); - const [ editAction, viewRevisionsAction ] = usePostActions( - onActionPerformed, - [ 'edit-post', 'view-post-revisions' ] - ); + const templatePartActions = usePostActions( TEMPLATE_PART_POST_TYPE ); + const patternActions = usePostActions( PATTERN_TYPES.user ); + const editAction = useEditPostAction(); + const actions = useMemo( () => { if ( type === TEMPLATE_PART_POST_TYPE ) { return [ editAction, - renameAction, + ...templatePartActions, duplicateTemplatePartAction, - viewRevisionsAction, resetAction, deleteAction, - ]; + ].filter( Boolean ); } return [ - renameAction, + editAction, + ...patternActions, duplicatePatternAction, exportJSONaction, resetAction, deleteAction, - ]; - }, [ type, editAction, viewRevisionsAction ] ); + ].filter( Boolean ); + }, [ editAction, type, templatePartActions, patternActions ] ); const onChangeView = useCallback( ( newView ) => { if ( newView.type !== view.type ) { diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index c0646981b3933..fc9326a219105 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -43,6 +43,7 @@ import { import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; +import { useEditPostAction } from '../dataviews-actions'; const { usePostActions } = unlock( editorPrivateApis ); @@ -183,14 +184,6 @@ function Preview( { item, viewType } ) { ); } -const TEMPLATE_ACTIONS = [ - 'edit-post', - 'reset-template', - 'rename-template', - 'view-post-revisions', - 'delete-template', -]; - export default function PageTemplates() { const { params } = useLocation(); const { activeView = 'all', layout } = params; @@ -330,22 +323,13 @@ export default function PageTemplates() { return filterSortAndPaginate( records, view, fields ); }, [ records, view, fields ] ); - const onActionPerformed = useCallback( - ( actionId, items ) => { - if ( actionId === 'edit-post' ) { - const post = items[ 0 ]; - history.push( { - postId: post.id, - postType: post.type, - canvas: 'edit', - } ); - } - }, - [ history ] + const postTypeActions = usePostActions( TEMPLATE_POST_TYPE ); + const editAction = useEditPostAction(); + const actions = useMemo( + () => [ editAction, ...postTypeActions ], + [ postTypeActions, editAction ] ); - const actions = usePostActions( onActionPerformed, TEMPLATE_ACTIONS ); - const onChangeView = useCallback( ( newView ) => { if ( newView.type !== view.type ) { diff --git a/packages/edit-site/src/components/revisions/index.js b/packages/edit-site/src/components/revisions/index.js index 2d4f332703585..b726e79b15f2f 100644 --- a/packages/edit-site/src/components/revisions/index.js +++ b/packages/edit-site/src/components/revisions/index.js @@ -10,6 +10,7 @@ import { __unstableEditorStyles as EditorStyles, __unstableIframe as Iframe, } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { useSelect } from '@wordpress/data'; import { useContext, useMemo } from '@wordpress/element'; @@ -18,7 +19,6 @@ import { useContext, useMemo } from '@wordpress/element'; */ import { unlock } from '../../lock-unlock'; -import { mergeBaseAndUserConfigs } from '../global-styles/global-styles-provider'; import EditorCanvasContainer from '../editor-canvas-container'; const { @@ -26,6 +26,7 @@ const { GlobalStylesContext, useGlobalStylesOutputWithConfig, } = unlock( blockEditorPrivateApis ); +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); function isObjectEmpty( object ) { return ! object || Object.keys( object ).length === 0; diff --git a/packages/edit-site/src/components/sidebar-edit-mode/index.js b/packages/edit-site/src/components/sidebar-edit-mode/index.js deleted file mode 100644 index 8541f952abbf4..0000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/index.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createSlotFill, - privateApis as componentsPrivateApis, -} from '@wordpress/components'; -import { isRTL, __ } from '@wordpress/i18n'; -import { drawerLeft, drawerRight } from '@wordpress/icons'; -import { useCallback, useContext, useEffect, useRef } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { store as coreStore } from '@wordpress/core-data'; -import { - PageAttributesPanel, - PostDiscussionPanel, - PostLastRevisionPanel, - PostTaxonomiesPanel, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import DefaultSidebar from './default-sidebar'; -import GlobalStylesSidebar from './global-styles-sidebar'; -import SettingsHeader from './settings-header'; -import PagePanels from './page-panels'; -import TemplatePanel from './template-panel'; -import { store as editSiteStore } from '../../store'; -import { unlock } from '../../lock-unlock'; - -const { Tabs } = unlock( componentsPrivateApis ); -const { interfaceStore, useAutoSwitchEditorSidebars, PatternOverridesPanel } = - unlock( editorPrivateApis ); -const { Slot: InspectorSlot, Fill: InspectorFill } = createSlotFill( - 'EditSiteSidebarInspector' -); -export const SidebarInspectorFill = InspectorFill; - -const FillContents = ( { tabName, isEditingPage, supportsGlobalStyles } ) => { - const tabListRef = useRef( null ); - // Because `DefaultSidebar` 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' ) === tabName - ); - 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(); - } - }, [ tabName ] ); - - return ( - <> - <DefaultSidebar - identifier={ tabName } - title={ __( 'Settings' ) } - icon={ isRTL() ? drawerLeft : drawerRight } - closeLabel={ __( 'Close Settings' ) } - header={ - <Tabs.Context.Provider value={ tabsContextValue }> - <SettingsHeader ref={ tabListRef } /> - </Tabs.Context.Provider> - } - headerClassName="edit-site-sidebar-edit-mode__panel-tabs" - // This classname is added so we can apply a corrective negative - // margin to the panel. - // see https://github.com/WordPress/gutenberg/pull/55360#pullrequestreview-1737671049 - className="edit-site-sidebar__panel" - isActiveByDefault - > - <Tabs.Context.Provider value={ tabsContextValue }> - <Tabs.TabPanel - tabId="edit-post/document" - focusable={ false } - > - { isEditingPage ? <PagePanels /> : <TemplatePanel /> } - <PostLastRevisionPanel /> - <PostTaxonomiesPanel /> - <PostDiscussionPanel /> - <PageAttributesPanel /> - <PatternOverridesPanel /> - </Tabs.TabPanel> - <Tabs.TabPanel tabId="edit-post/block" focusable={ false }> - <InspectorSlot bubblesVirtually /> - </Tabs.TabPanel> - </Tabs.Context.Provider> - </DefaultSidebar> - { supportsGlobalStyles && <GlobalStylesSidebar /> } - </> - ); -}; - -export function SidebarComplementaryAreaFills() { - useAutoSwitchEditorSidebars(); - const { tabName, supportsGlobalStyles, isEditingPage } = useSelect( - ( select ) => { - const sidebar = - select( interfaceStore ).getActiveComplementaryArea( 'core' ); - - const _isEditorSidebarOpened = [ - 'edit-post/block', - 'edit-post/document', - ].includes( sidebar ); - let _tabName = sidebar; - if ( ! _isEditorSidebarOpened ) { - _tabName = !! select( - blockEditorStore - ).getBlockSelectionStart() - ? 'edit-post/block' - : 'edit-post/document'; - } - - return { - tabName: _tabName, - supportsGlobalStyles: - select( coreStore ).getCurrentTheme()?.is_block_theme, - isEditingPage: select( editSiteStore ).isPage(), - }; - }, - [] - ); - const { enableComplementaryArea } = useDispatch( interfaceStore ); - - // `newSelectedTabId` could technically be falsey if no tab is selected (i.e. - // the initial render) or when we don't want a tab displayed (i.e. the - // sidebar is closed). These cases should both be covered by the `!!` check - // below, so we shouldn't need any additional falsey handling. - const onTabSelect = useCallback( - ( newSelectedTabId ) => { - if ( !! newSelectedTabId ) { - enableComplementaryArea( 'core', newSelectedTabId ); - } - }, - [ enableComplementaryArea ] - ); - - return ( - <Tabs - selectedTabId={ tabName } - onSelect={ onTabSelect } - selectOnMove={ false } - > - <FillContents - tabName={ tabName } - isEditingPage={ isEditingPage } - supportsGlobalStyles={ supportsGlobalStyles } - /> - </Tabs> - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js deleted file mode 100644 index f4fc2af178e08..0000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ /dev/null @@ -1,133 +0,0 @@ -/** - * WordPress dependencies - */ -import { PanelBody } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { - PluginDocumentSettingPanel, - store as editorStore, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { useCallback } from '@wordpress/element'; -import { store as noticesStore } from '@wordpress/notices'; - -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../../store'; -import PageContent from './page-content'; -import PageSummary from './page-summary'; - -import { unlock } from '../../../lock-unlock'; - -const { PostCardPanel, PostActions } = unlock( editorPrivateApis ); -const { useHistory } = unlock( routerPrivateApis ); - -export default function PagePanels() { - const { id, type, hasResolved, status, date, password, renderingMode } = - useSelect( ( select ) => { - const { getEditedPostContext } = select( editSiteStore ); - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const { getRenderingMode } = select( editorStore ); - const context = getEditedPostContext(); - const queryArgs = [ 'postType', context.postType, context.postId ]; - const page = getEditedEntityRecord( ...queryArgs ); - return { - hasResolved: hasFinishedResolution( - 'getEditedEntityRecord', - queryArgs - ), - id: page?.id, - type: page?.type, - status: page?.status, - date: page?.date, - password: page?.password, - renderingMode: getRenderingMode(), - }; - }, [] ); - const { createSuccessNotice } = useDispatch( noticesStore ); - const history = useHistory(); - const onActionPerformed = useCallback( - ( actionId, items ) => { - switch ( actionId ) { - case 'move-to-trash': - { - history.push( { - path: '/' + items[ 0 ].type, - postId: undefined, - postType: undefined, - canvas: 'view', - } ); - } - break; - case 'duplicate-post': - { - const newItem = items[ 0 ]; - const title = - typeof newItem.title === 'string' - ? newItem.title - : newItem.title?.rendered; - createSuccessNotice( - sprintf( - // translators: %s: Title of the created post e.g: "Post 1". - __( '"%s" successfully created.' ), - title - ), - { - type: 'snackbar', - id: 'duplicate-post-action', - actions: [ - { - label: __( 'Edit' ), - onClick: () => { - history.push( { - path: undefined, - postId: newItem.id, - postType: newItem.type, - canvas: 'edit', - } ); - }, - }, - ], - } - ); - } - break; - } - }, - [ history, createSuccessNotice ] - ); - - if ( ! hasResolved ) { - return null; - } - - return ( - <> - <PostCardPanel - actions={ - <PostActions onActionPerformed={ onActionPerformed } /> - } - /> - <PanelBody title={ __( 'Summary' ) }> - <PageSummary - status={ status } - date={ date } - password={ password } - postId={ id } - postType={ type } - /> - </PanelBody> - <PluginDocumentSettingPanel.Slot /> - { renderingMode !== 'post-only' && ( - <PanelBody title={ __( 'Content' ) }> - <PageContent /> - </PanelBody> - ) } - </> - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js deleted file mode 100644 index 0830ff8364c8a..0000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * WordPress dependencies - */ -import { __experimentalVStack as VStack } from '@wordpress/components'; -import { - PluginPostStatusInfo, - PostAuthorPanel, - PostURLPanel, - PostSchedulePanel, - PostTemplatePanel, - PostFeaturedImagePanel, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { unlock } from '../../../lock-unlock'; - -const { - PrivatePostExcerptPanel, - PostStatus, - PostContentInformation, - PostLastEditedPanel, -} = unlock( editorPrivateApis ); - -export default function PageSummary() { - return ( - <VStack spacing={ 0 }> - <PluginPostStatusInfo.Slot> - { ( fills ) => ( - <> - <VStack - spacing={ 3 } - // TODO: this needs to be consolidated with the panel in post editor, when we unify them. - style={ { marginBlockEnd: '24px' } } - > - <PostFeaturedImagePanel withPanelBody={ false } /> - <PrivatePostExcerptPanel /> - <VStack spacing={ 1 }> - <PostContentInformation /> - <PostLastEditedPanel /> - </VStack> - </VStack> - <VStack - spacing={ 1 } - style={ { marginBlockEnd: '12px' } } - > - <PostStatus /> - <PostSchedulePanel /> - <PostTemplatePanel /> - <PostURLPanel /> - </VStack> - <PostAuthorPanel /> - { fills } - </> - ) } - </PluginPostStatusInfo.Slot> - </VStack> - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js deleted file mode 100644 index 054952077a491..0000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * WordPress dependencies - */ -import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; -import { forwardRef } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { unlock } from '../../../lock-unlock'; - -const { Tabs } = unlock( componentsPrivateApis ); - -const SettingsHeader = ( _, ref ) => { - const postTypeLabel = useSelect( - ( select ) => select( editorStore ).getPostTypeLabel(), - [] - ); - - return ( - <Tabs.TabList ref={ ref }> - <Tabs.Tab - tabId="edit-post/document" - // Used for focus management in the SettingsSidebar component. - data-tab-id="edit-post/document" - > - { postTypeLabel } - </Tabs.Tab> - <Tabs.Tab - tabId="edit-post/block" - // Used for focus management in the SettingsSidebar component. - data-tab-id="edit-post/block" - > - { __( 'Block' ) } - </Tabs.Tab> - </Tabs.TabList> - ); -}; - -export default forwardRef( SettingsHeader ); diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js deleted file mode 100644 index f0abc9aed09d0..0000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { PanelBody, PanelRow } from '@wordpress/components'; -import { - PluginDocumentSettingPanel, - privateApis as editorPrivateApis, - store as editorStore, -} from '@wordpress/editor'; -import { store as coreStore } from '@wordpress/core-data'; -import { __ } from '@wordpress/i18n'; -import { useAsyncList } from '@wordpress/compose'; -import { serialize } from '@wordpress/blocks'; -import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../../store'; -import TemplateActions from '../../template-actions'; -import PluginTemplateSettingPanel from '../../plugin-template-setting-panel'; -import { useAvailablePatterns } from './hooks'; -import { TEMPLATE_PART_POST_TYPE } from '../../../utils/constants'; -import { unlock } from '../../../lock-unlock'; - -const { PostCardPanel } = unlock( editorPrivateApis ); -const { useHistory } = unlock( routerPrivateApis ); - -function TemplatesList( { availableTemplates, onSelect } ) { - const shownTemplates = useAsyncList( availableTemplates ); - if ( ! availableTemplates || availableTemplates?.length === 0 ) { - return null; - } - - return ( - <BlockPatternsList - label={ __( 'Templates' ) } - blockPatterns={ availableTemplates } - shownPatterns={ shownTemplates } - onClickPattern={ onSelect } - showTitlesAsTooltip - /> - ); -} - -const POST_TYPE_PATH = { - wp_template: '/wp_template', - wp_template_part: '/patterns', -}; - -export default function TemplatePanel() { - const { title, description, record, postType, postId } = useSelect( - ( select ) => { - const { getEditedPostType, getEditedPostId } = - select( editSiteStore ); - const { getEditedEntityRecord } = select( coreStore ); - const { __experimentalGetTemplateInfo: getTemplateInfo } = - select( editorStore ); - - const type = getEditedPostType(); - const _postId = getEditedPostId(); - const _record = getEditedEntityRecord( 'postType', type, _postId ); - const info = getTemplateInfo( _record ); - - return { - title: info.title, - description: info.description, - icon: info.icon, - record: _record, - postType: type, - postId: _postId, - }; - }, - [] - ); - const history = useHistory(); - const availablePatterns = useAvailablePatterns( record ); - const { editEntityRecord } = useDispatch( coreStore ); - - if ( ! title && ! description ) { - return null; - } - - const onTemplateSelect = async ( selectedTemplate ) => { - await editEntityRecord( 'postType', postType, postId, { - blocks: selectedTemplate.blocks, - content: serialize( selectedTemplate.blocks ), - } ); - }; - - return ( - <> - <PostCardPanel - className="edit-site-template-card" - actions={ - <TemplateActions - postType={ postType } - postId={ postId } - className="edit-site-template-card__actions" - toggleProps={ { size: 'small' } } - onRemove={ () => { - history.push( { - path: POST_TYPE_PATH[ postType ], - } ); - } } - /> - } - /> - <PluginTemplateSettingPanel.Slot /> - <PluginDocumentSettingPanel.Slot /> - { availablePatterns?.length > 0 && ( - <PanelBody - title={ __( 'Transform into:' ) } - initialOpen={ postType === TEMPLATE_PART_POST_TYPE } - > - <PanelRow> - <p> - { __( - 'Choose a predefined pattern to switch up the look of your template.' // TODO - make this dynamic? - ) } - </p> - </PanelRow> - - <TemplatesList - availableTemplates={ availablePatterns } - onSelect={ onTemplateSelect } - /> - </PanelBody> - ) } - </> - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss deleted file mode 100644 index 15a0541672b1d..0000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss +++ /dev/null @@ -1,16 +0,0 @@ -.edit-site-template-card { - &__actions { - line-height: 0; - > .components-button.is-small.has-icon { - padding: 0; - min-width: auto; - } - flex-shrink: 0; - } -} - - -.edit-site-template-panel .block-editor-block-preview__container { - border-radius: 2px; - box-shadow: none; -} diff --git a/packages/edit-site/src/components/sidebar-navigation-item/index.js b/packages/edit-site/src/components/sidebar-navigation-item/index.js index 69a2af4a45de4..8e979e7ab8bde 100644 --- a/packages/edit-site/src/components/sidebar-navigation-item/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-item/index.js @@ -35,7 +35,7 @@ export default function SidebarNavigationItem( { ...props } ) { const history = useHistory(); - const navigate = useContext( SidebarNavigationContext ); + const { navigate } = useContext( SidebarNavigationContext ); // If there is no custom click handler, create one that navigates to `path`. function handleClick( e ) { diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js index f0585b410e40d..87a7b6d9d15f2 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js @@ -103,7 +103,7 @@ function SidebarNavigationScreenGlobalStylesContent() { ); } -export default function SidebarNavigationScreenGlobalStyles() { +export default function SidebarNavigationScreenGlobalStyles( { backPath } ) { const { revisions, isLoading: isLoadingRevisions } = useGlobalStylesRevisions(); const { openGeneralSidebar } = useDispatch( editSiteStore ); @@ -179,6 +179,7 @@ export default function SidebarNavigationScreenGlobalStyles() { description={ __( 'Choose a different style combination for the theme styles.' ) } + backPath={ backPath } content={ <SidebarNavigationScreenGlobalStylesContent /> } footer={ shouldShowGlobalStylesFooter && ( diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/index.js index 2fcac0250c2bf..24fb9372b4fff 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/index.js @@ -22,7 +22,7 @@ const { useLocation } = unlock( routerPrivateApis ); export const postType = `wp_navigation`; -export default function SidebarNavigationScreenNavigationMenu() { +export default function SidebarNavigationScreenNavigationMenu( { backPath } ) { const { params: { postId }, } = useLocation(); @@ -67,6 +67,7 @@ export default function SidebarNavigationScreenNavigationMenu() { description={ __( 'Navigation Menus are a curated collection of blocks that allow visitors to get around your site.' ) } + backPath={ backPath } > <Spinner className="edit-site-sidebar-navigation-screen-navigation-menus__loading" /> </SidebarNavigationScreenWrapper> @@ -77,6 +78,7 @@ export default function SidebarNavigationScreenNavigationMenu() { return ( <SidebarNavigationScreenWrapper description={ __( 'Navigation Menu missing.' ) } + backPath={ backPath } /> ); } @@ -93,6 +95,7 @@ export default function SidebarNavigationScreenNavigationMenu() { onDuplicate={ _handleDuplicate } /> } + backPath={ backPath } title={ buildNavigationLabel( navigationMenu?.title, navigationMenu?.id, @@ -106,6 +109,7 @@ export default function SidebarNavigationScreenNavigationMenu() { return ( <SingleNavigationMenu navigationMenu={ navigationMenu } + backPath={ backPath } handleDelete={ _handleDelete } handleSave={ _handleSave } handleDuplicate={ _handleDuplicate } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js index 8a1d08850c4b0..d503869c8d874 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js @@ -13,6 +13,7 @@ import buildNavigationLabel from '../sidebar-navigation-screen-navigation-menus/ export default function SingleNavigationMenu( { navigationMenu, + backPath, handleDelete, handleDuplicate, handleSave, @@ -32,6 +33,7 @@ export default function SingleNavigationMenu( { /> </> } + backPath={ backPath } title={ buildNavigationLabel( navigationMenu?.title, navigationMenu?.id, diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js index 65f80349ddaca..d5c3fa92c902f 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/index.js @@ -46,7 +46,7 @@ function buildMenuLabel( title, id, status ) { // Save a boolean to prevent us creating a fallback more than once per session. let hasCreatedFallback = false; -export default function SidebarNavigationScreenNavigationMenus() { +export default function SidebarNavigationScreenNavigationMenus( { backPath } ) { const { records: navigationMenus, isResolving: isResolvingNavigationMenus, @@ -87,7 +87,7 @@ export default function SidebarNavigationScreenNavigationMenus() { if ( isLoading ) { return ( - <SidebarNavigationScreenWrapper> + <SidebarNavigationScreenWrapper backPath={ backPath }> <Spinner className="edit-site-sidebar-navigation-screen-navigation-menus__loading" /> </SidebarNavigationScreenWrapper> ); @@ -97,6 +97,7 @@ export default function SidebarNavigationScreenNavigationMenus() { return ( <SidebarNavigationScreenWrapper description={ __( 'No Navigation Menus found.' ) } + backPath={ backPath } /> ); } @@ -106,6 +107,7 @@ export default function SidebarNavigationScreenNavigationMenus() { return ( <SingleNavigationMenu navigationMenu={ firstNavigationMenu } + backPath={ backPath } handleDelete={ () => handleDelete( firstNavigationMenu ) } handleDuplicate={ () => handleDuplicate( firstNavigationMenu ) } handleSave={ ( edits ) => @@ -116,7 +118,7 @@ export default function SidebarNavigationScreenNavigationMenus() { } return ( - <SidebarNavigationScreenWrapper> + <SidebarNavigationScreenWrapper backPath={ backPath }> <ItemGroup> { navigationMenus?.map( ( { id, title, status }, index ) => ( <NavMenuItem @@ -138,12 +140,14 @@ export function SidebarNavigationScreenWrapper( { actions, title, description, + backPath, } ) { return ( <SidebarNavigationScreen title={ title || __( 'Navigation' ) } actions={ actions } description={ description || __( 'Manage your Navigation Menus.' ) } + backPath={ backPath } content={ children } /> ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js index 1e63225641895..bb34802ebbbc3 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menus/leaf-more-menu.js @@ -18,10 +18,6 @@ const POPOVER_PROPS = { /** * Internal dependencies */ -import { - isPreviewingTheme, - currentlyPreviewingTheme, -} from '../../utils/is-previewing-theme'; import { unlock } from '../../lock-unlock'; const { useLocation, useHistory } = unlock( routerPrivateApis ); @@ -68,9 +64,6 @@ export default function LeafMoreMenu( props ) { { postType: attributes.type, postId: attributes.id, - ...( isPreviewingTheme() && { - wp_theme_preview: currentlyPreviewingTheme(), - } ), }, { backPath: params, @@ -82,9 +75,6 @@ export default function LeafMoreMenu( props ) { { postType: 'page', postId: attributes.id, - ...( isPreviewingTheme() && { - wp_theme_preview: currentlyPreviewingTheme(), - } ), }, { backPath: params, diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/index.js index 6058b7d907d82..1b21466576f8e 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pattern/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pattern/index.js @@ -19,11 +19,11 @@ import TemplateActions from '../template-actions'; const { useLocation, useHistory } = unlock( routerPrivateApis ); -export default function SidebarNavigationScreenPattern() { +export default function SidebarNavigationScreenPattern( { backPath } ) { const history = useHistory(); const location = useLocation(); const { - params: { postType, postId, categoryId, categoryType }, + params: { postType, postId }, } = location; const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); @@ -31,12 +31,6 @@ export default function SidebarNavigationScreenPattern() { const patternDetails = usePatternDetails( postType, postId ); - const backPath = { - categoryId, - categoryType, - path: '/patterns', - }; - return ( <SidebarNavigationScreen actions={ diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js index 352c0c4c455ec..ee01a427e6dc9 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/index.js @@ -110,7 +110,7 @@ function CategoriesGroup( { ); } -export default function SidebarNavigationScreenPatterns() { +export default function SidebarNavigationScreenPatterns( { backPath } ) { const { params: { categoryType, categoryId, path }, } = useLocation(); @@ -144,6 +144,7 @@ export default function SidebarNavigationScreenPatterns() { description={ __( 'Manage what patterns are available when editing the site.' ) } + backPath={ backPath } actions={ <AddNewPattern /> } content={ <> diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js index ff12434b27895..76641f969fca7 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js @@ -94,7 +94,7 @@ function useTemplateDetails( postType, postId ) { return { title, description, content, footer }; } -export default function SidebarNavigationScreenTemplate() { +export default function SidebarNavigationScreenTemplate( { backPath } ) { const history = useHistory(); const { params: { postType, postId }, @@ -108,6 +108,7 @@ export default function SidebarNavigationScreenTemplate() { return ( <SidebarNavigationScreen title={ title } + backPath={ backPath } actions={ <> <TemplateActions diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/index.js index d48314d1f36e6..cf7b4fa114cf4 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/index.js @@ -13,7 +13,7 @@ import DataviewsTemplatesSidebarContent from './content'; const { useLocation } = unlock( routerPrivateApis ); -export default function SidebarNavigationScreenTemplatesBrowse() { +export default function SidebarNavigationScreenTemplatesBrowse( { backPath } ) { const { params: { activeView = 'all' }, } = useLocation(); @@ -24,6 +24,7 @@ export default function SidebarNavigationScreenTemplatesBrowse() { description={ __( 'Create new templates, or reset any customizations made to the templates supplied by your theme.' ) } + backPath={ backPath } content={ <DataviewsTemplatesSidebarContent activeView={ activeView } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/index.js b/packages/edit-site/src/components/sidebar-navigation-screen/index.js index ef7605e003cdb..19e3335f9ff06 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen/index.js @@ -32,32 +32,6 @@ import { SidebarNavigationContext } from '../sidebar'; const { useHistory, useLocation } = unlock( routerPrivateApis ); -function getBackPath( params ) { - // Navigation Menus are not currently part of a data view. - // Therefore when navigating back from a navigation menu - // the target path is the navigation listing view. - if ( params.path === '/navigation' && params.postId ) { - return { path: '/navigation' }; - } - - // From a data view path we navigate back to root - if ( params.path ) { - return {}; - } - - // From edit screen for a post we navigate back to post-type specific data view - if ( params.postType === 'page' ) { - return { path: '/page', postId: params.postId }; - } else if ( params.postType === 'wp_template' ) { - return { path: '/wp_template', postId: params.postId }; - } else if ( params.postType === 'wp_navigation' ) { - return { path: '/navigation', postId: params.postId }; - } - - // Go back to root by default - return {}; -} - export default function SidebarNavigationScreen( { isRoot, title, @@ -88,8 +62,10 @@ export default function SidebarNavigationScreen( { ); const location = useLocation(); const history = useHistory(); - const navigate = useContext( SidebarNavigationContext ); + const { navigate } = useContext( SidebarNavigationContext ); + const backPath = backPathProp ?? location.state?.backPath; const icon = isRTL() ? chevronRight : chevronLeft; + return ( <> <VStack @@ -107,10 +83,6 @@ export default function SidebarNavigationScreen( { { ! isRoot && ( <SidebarButton onClick={ () => { - const backPath = - backPathProp ?? - location.state?.backPath ?? - getBackPath( location.params ); history.push( backPath ); navigate( 'back' ); } } diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index a246acaa46ce7..84820952e1b62 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -7,62 +7,86 @@ import clsx from 'clsx'; * WordPress dependencies */ import { - useCallback, createContext, + useContext, useState, useRef, - useEffect, + useLayoutEffect, } from '@wordpress/element'; import { focus } from '@wordpress/dom'; export const SidebarNavigationContext = createContext( () => {} ); +// Focus a sidebar element after a navigation. The element to focus is either +// specified by `focusSelector` (when navigating back) or it is the first +// tabbable element (usually the "Back" button). +function focusSidebarElement( el, direction, focusSelector ) { + let elementToFocus; + if ( direction === 'back' && focusSelector ) { + elementToFocus = el.querySelector( focusSelector ); + } + if ( direction !== null && ! elementToFocus ) { + const [ firstTabbable ] = focus.tabbable.find( el ); + elementToFocus = firstTabbable ?? el; + } + elementToFocus?.focus(); +} -export default function SidebarContent( { routeKey, children } ) { - const [ navState, setNavState ] = useState( { +// Navigation state that is updated when navigating back or forward. Helps us +// manage the animations and also focus. +function createNavState() { + let state = { direction: null, focusSelector: null, - } ); + }; - const navigate = useCallback( ( direction, focusSelector = null ) => { - setNavState( ( prevState ) => ( { - direction, - focusSelector: - direction === 'forward' && focusSelector - ? focusSelector - : prevState.focusSelector, - } ) ); - }, [] ); + return { + get() { + return state; + }, + navigate( direction, focusSelector = null ) { + state = { + direction, + focusSelector: + direction === 'forward' && focusSelector + ? focusSelector + : state.focusSelector, + }; + }, + }; +} +function SidebarContentWrapper( { children } ) { + const navState = useContext( SidebarNavigationContext ); const wrapperRef = useRef(); - useEffect( () => { - let elementToFocus; - if ( navState.direction === 'back' && navState.focusSelector ) { - elementToFocus = wrapperRef.current.querySelector( - navState.focusSelector - ); - } - if ( navState.direction !== null && ! elementToFocus ) { - const [ firstTabbable ] = focus.tabbable.find( wrapperRef.current ); - elementToFocus = firstTabbable ?? wrapperRef.current; - } - elementToFocus?.focus(); + const [ navAnimation, setNavAnimation ] = useState( null ); + + useLayoutEffect( () => { + const { direction, focusSelector } = navState.get(); + focusSidebarElement( wrapperRef.current, direction, focusSelector ); + setNavAnimation( direction ); }, [ navState ] ); const wrapperCls = clsx( 'edit-site-sidebar__screen-wrapper', { - 'slide-from-left': navState.direction === 'back', - 'slide-from-right': navState.direction === 'forward', + 'slide-from-left': navAnimation === 'back', + 'slide-from-right': navAnimation === 'forward', } ); return ( - <SidebarNavigationContext.Provider value={ navigate }> + <div ref={ wrapperRef } className={ wrapperCls }> + { children } + </div> + ); +} + +export default function SidebarContent( { routeKey, children } ) { + const [ navState ] = useState( createNavState ); + + return ( + <SidebarNavigationContext.Provider value={ navState }> <div className="edit-site-sidebar__content"> - <div - ref={ wrapperRef } - key={ routeKey } - className={ wrapperCls } - > + <SidebarContentWrapper key={ routeKey }> { children } - </div> + </SidebarContentWrapper> </div> </SidebarNavigationContext.Provider> ); diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index c807c11ca369e..900fa47beb99c 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -24,6 +24,7 @@ import { __unstableEditorStyles as EditorStyles, __unstableIframe as Iframe, } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { useSelect } from '@wordpress/data'; import { useResizeObserver } from '@wordpress/compose'; import { useMemo, useState, memo, useContext } from '@wordpress/element'; @@ -34,7 +35,6 @@ import { ENTER, SPACE } from '@wordpress/keycodes'; */ import { unlock } from '../../lock-unlock'; import EditorCanvasContainer from '../editor-canvas-container'; -import { mergeBaseAndUserConfigs } from '../global-styles/global-styles-provider'; const { ExperimentalBlockEditorProvider, @@ -42,6 +42,7 @@ const { GlobalStylesContext, useGlobalStylesOutputWithConfig, } = unlock( blockEditorPrivateApis ); +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); const { CompositeV2: Composite, diff --git a/packages/edit-site/src/components/template-part-converter/index.js b/packages/edit-site/src/components/template-part-converter/index.js index 7694735cbb302..de47eb6ae26f4 100644 --- a/packages/edit-site/src/components/template-part-converter/index.js +++ b/packages/edit-site/src/components/template-part-converter/index.js @@ -27,12 +27,25 @@ export default function TemplatePartConverter() { } function TemplatePartConverterMenuItem( { clientIds, onClose } ) { - const blocks = useSelect( - ( select ) => - select( blockEditorStore ).getBlocksByClientId( clientIds ), + const { isContentOnly, blocks } = useSelect( + ( select ) => { + const { getBlocksByClientId, getBlockEditingMode } = + select( blockEditorStore ); + return { + blocks: getBlocksByClientId( clientIds ), + isContentOnly: + clientIds.length === 1 && + getBlockEditingMode( clientIds[ 0 ] ) === 'contentOnly', + }; + }, [ clientIds ] ); + // Do not show the convert button if the block is in content-only mode. + if ( isContentOnly ) { + return null; + } + // Allow converting a single template part to standard blocks. if ( blocks.length === 1 && blocks[ 0 ]?.name === 'core/template-part' ) { return ( diff --git a/packages/edit-site/src/hooks/use-theme-style-variations/use-theme-style-variations-by-property.js b/packages/edit-site/src/hooks/use-theme-style-variations/use-theme-style-variations-by-property.js index 27c3adf438ad6..d14cd8f64b2cc 100644 --- a/packages/edit-site/src/hooks/use-theme-style-variations/use-theme-style-variations-by-property.js +++ b/packages/edit-site/src/hooks/use-theme-style-variations/use-theme-style-variations-by-property.js @@ -5,16 +5,17 @@ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { useContext, useMemo } from '@wordpress/element'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { mergeBaseAndUserConfigs } from '../../components/global-styles/global-styles-provider'; import cloneDeep from '../../utils/clone-deep'; import { unlock } from '../../lock-unlock'; const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); +const { mergeBaseAndUserConfigs } = unlock( editorPrivateApis ); /** * Removes all instances of a property from an object. diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index d0002e1070ce3..42b942bca4f01 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -6,15 +6,13 @@ @import "./components/code-editor/style.scss"; @import "./components/global-styles/style.scss"; @import "./components/global-styles/screen-revisions/style.scss"; +@import "./components/global-styles-sidebar/style.scss"; @import "./components/header-edit-mode/style.scss"; @import "./components/page/style.scss"; @import "./components/page-pages/style.scss"; @import "./components/page-patterns/style.scss"; @import "./components/page-templates/style.scss"; @import "./components/table/style.scss"; -@import "./components/sidebar-edit-mode/style.scss"; -@import "./components/sidebar-edit-mode/settings-header/style.scss"; -@import "./components/sidebar-edit-mode/template-panel/style.scss"; @import "./components/editor/style.scss"; @import "./components/create-template-part-modal/style.scss"; @import "./components/welcome-guide/style.scss"; @@ -85,13 +83,6 @@ body.js.site-editor-php { .interface-interface-skeleton { top: 0; } - - // Todo: Remove this rule when edit site gets support - // for opening unpinned sidebar items. - .interface-complementary-area__pin-unpin-item.components-button { - display: none; - } - } /** diff --git a/packages/edit-site/src/utils/math.js b/packages/edit-site/src/utils/math.js deleted file mode 100644 index 8c4cca4f5e111..0000000000000 --- a/packages/edit-site/src/utils/math.js +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @typedef {Object} WPPoint - * @property {number} x The horizontal coordinate. - * @property {number} y The vertical coordinate. - */ - -/** - * Clamps a value to a range. Uses the CSS `clamp()` ordering for arguments. - * - * @param {number} min Minimum range. - * @param {number} value Value to clamp. - * @param {number} max Maximum range. - * - * @return {number} Clamped value. - */ -function clamp( min, value, max ) { - return Math.max( min, Math.min( value, max ) ); -} - -/** - * Evaluates a linear function passing through two points at the given x value. - * - * Example: - * f(x) - * │ ╲ - * │ * (p0) - * │ ╲ - * │ ╲ - * │ (p1) * - * │ ╲ - * └──────────── x - * - * @param {WPPoint} p0 First point. - * @param {WPPoint} p1 Second point. - * @param {number} x Value to evaluate at. - * - * @return {number} Result of the two-point linear function at x. - */ -function twoPointLinearFn( p0, p1, x ) { - return ( ( p1.y - p0.y ) / ( p1.x - p0.x ) ) * ( x - p0.x ) + p0.y; -} - -/** - * Evaluates a two-point linear function at a given x value, clamped to the range of the points. - * - * Example: - * f(x) - * │ ───* (p0) - * │ ╲ - * │ ╲ - * │ (p1) *─── - * └──────────── x - * - * @param {WPPoint} p0 First point. - * @param {WPPoint} p1 Second point. - * @param {number} x Value to evaluate at. - * - * @return {number} Result of the two-point linear function clamped to the range of the points. - */ -function clampedTwoPointLinearFn( p0, p1, x ) { - return clamp( - Math.min( p0.y, p1.y ), - twoPointLinearFn( p0, p1, x ), - Math.max( p0.y, p1.y ) - ); -} - -/** - * Computes the iframe scale using a start and end width/scale pair and the current width. - * - * The scale is clamped outside the points and is linearly interpolated between. - * - * Example: - * scale - * │ ───* (start) - * │ ╲ - * │ ╲ - * │ (end) *─── - * └──────────── width - * - * @param {Object} start First width and scale pair. - * @param {Object} end Second width and scale pair. - * @param {number} currentWidth Current width. - * - * @return {number} The scale of the current width between the two points. - */ -export function computeIFrameScale( start, end, currentWidth ) { - return clampedTwoPointLinearFn( - { x: start.width, y: start.scale }, - { x: end.width, y: end.scale }, - currentWidth - ); -} diff --git a/packages/edit-site/src/utils/use-activate-theme.js b/packages/edit-site/src/utils/use-activate-theme.js index d201f630fd6dc..e5f9488a3edd8 100644 --- a/packages/edit-site/src/utils/use-activate-theme.js +++ b/packages/edit-site/src/utils/use-activate-theme.js @@ -23,7 +23,7 @@ const { useHistory, useLocation } = unlock( routerPrivateApis ); */ export function useActivateTheme() { const history = useHistory(); - const location = useLocation(); + const { params } = useLocation(); const { startResolution, finishResolution } = useDispatch( coreStore ); return async () => { @@ -36,9 +36,9 @@ export function useActivateTheme() { startResolution( 'activateTheme' ); await window.fetch( activationURL ); finishResolution( 'activateTheme' ); - const { wp_theme_preview: themePreview, ...params } = - location.params; - history.replace( params ); + // Remove the wp_theme_preview query param: we've finished activating + // the queue and are switching to normal Site Editor. + history.replace( { ...params, wp_theme_preview: undefined } ); } }; } diff --git a/packages/edit-widgets/src/components/header/document-tools/index.js b/packages/edit-widgets/src/components/header/document-tools/index.js index bd1b8fc7adae7..a0d69cde376cf 100644 --- a/packages/edit-widgets/src/components/header/document-tools/index.js +++ b/packages/edit-widgets/src/components/header/document-tools/index.js @@ -4,12 +4,9 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { __, _x } from '@wordpress/i18n'; import { Button, ToolbarItem } from '@wordpress/components'; -import { - NavigableToolbar, - store as blockEditorStore, -} from '@wordpress/block-editor'; +import { NavigableToolbar } from '@wordpress/block-editor'; import { listView, plus } from '@wordpress/icons'; -import { useCallback, useRef } from '@wordpress/element'; +import { useCallback } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; /** @@ -17,61 +14,44 @@ import { useViewportMatch } from '@wordpress/compose'; */ import UndoButton from '../undo-redo/undo'; import RedoButton from '../undo-redo/redo'; -import useLastSelectedWidgetArea from '../../../hooks/use-last-selected-widget-area'; import { store as editWidgetsStore } from '../../../store'; import { unlock } from '../../../lock-unlock'; function DocumentTools() { const isMediumViewport = useViewportMatch( 'medium' ); - const inserterButton = useRef(); - const widgetAreaClientId = useLastSelectedWidgetArea(); - const isLastSelectedWidgetAreaOpen = useSelect( - ( select ) => - select( editWidgetsStore ).getIsWidgetAreaOpen( - widgetAreaClientId - ), - [ widgetAreaClientId ] - ); - const { isInserterOpen, isListViewOpen, listViewToggleRef } = useSelect( - ( select ) => { - const { isInserterOpened, isListViewOpened, getListViewToggleRef } = - unlock( select( editWidgetsStore ) ); - return { - isInserterOpen: isInserterOpened(), - isListViewOpen: isListViewOpened(), - listViewToggleRef: getListViewToggleRef(), - }; - }, - [] - ); - const { setIsWidgetAreaOpen, setIsInserterOpened, setIsListViewOpened } = + + const { + isInserterOpen, + isListViewOpen, + inserterSidebarToggleRef, + listViewToggleRef, + } = useSelect( ( select ) => { + const { + isInserterOpened, + getInserterSidebarToggleRef, + isListViewOpened, + getListViewToggleRef, + } = unlock( select( editWidgetsStore ) ); + return { + isInserterOpen: isInserterOpened(), + isListViewOpen: isListViewOpened(), + inserterSidebarToggleRef: getInserterSidebarToggleRef(), + listViewToggleRef: getListViewToggleRef(), + }; + }, [] ); + const { setIsInserterOpened, setIsListViewOpened } = useDispatch( editWidgetsStore ); - const { selectBlock } = useDispatch( blockEditorStore ); - const handleClick = () => { - if ( isInserterOpen ) { - // Focusing the inserter button closes the inserter popover. - setIsInserterOpened( false ); - } else { - if ( ! isLastSelectedWidgetAreaOpen ) { - // Select the last selected block if hasn't already. - selectBlock( widgetAreaClientId ); - // Open the last selected widget area when opening the inserter. - setIsWidgetAreaOpen( widgetAreaClientId, true ); - } - // The DOM updates resulting from selectBlock() and setIsInserterOpened() calls are applied the - // same tick and pretty much in a random order. The inserter is closed if any other part of the - // app receives focus. If selectBlock() happens to take effect after setIsInserterOpened() then - // the inserter is visible for a brief moment and then gets auto-closed due to focus moving to - // the selected block. - window.requestAnimationFrame( () => setIsInserterOpened( true ) ); - } - }; const toggleListView = useCallback( () => setIsListViewOpened( ! isListViewOpen ), [ setIsListViewOpened, isListViewOpen ] ); + const toggleInserterSidebar = useCallback( + () => setIsInserterOpened( ! isInserterOpen ), + [ setIsInserterOpened, isInserterOpen ] + ); + return ( <NavigableToolbar className="edit-widgets-header-toolbar" @@ -79,7 +59,7 @@ function DocumentTools() { variant="unstyled" > <ToolbarItem - ref={ inserterButton } + ref={ inserterSidebarToggleRef } as={ Button } className="edit-widgets-header-toolbar__inserter-toggle" variant="primary" @@ -87,7 +67,7 @@ function DocumentTools() { onMouseDown={ ( event ) => { event.preventDefault(); } } - onClick={ handleClick } + onClick={ toggleInserterSidebar } icon={ plus } /* translators: button label text should, if possible, be under 16 characters. */ diff --git a/packages/edit-widgets/src/components/layout/style.scss b/packages/edit-widgets/src/components/layout/style.scss index a10665f7cafe7..4f5bb8834593d 100644 --- a/packages/edit-widgets/src/components/layout/style.scss +++ b/packages/edit-widgets/src/components/layout/style.scss @@ -18,7 +18,7 @@ // Leave space for the close button height: calc(100% - #{$button-size} - #{$grid-unit-10}); - .block-editor-inserter__tab { + .block-editor-inserter-sidebar__header { display: none; } diff --git a/packages/edit-widgets/src/store/private-selectors.js b/packages/edit-widgets/src/store/private-selectors.js index fca6aa5ddb759..ee2000ac12844 100644 --- a/packages/edit-widgets/src/store/private-selectors.js +++ b/packages/edit-widgets/src/store/private-selectors.js @@ -1,3 +1,7 @@ export function getListViewToggleRef( state ) { return state.listViewToggleRef; } + +export function getInserterSidebarToggleRef( state ) { + return state.inserterSidebarToggleRef; +} diff --git a/packages/edit-widgets/src/store/reducer.js b/packages/edit-widgets/src/store/reducer.js index 64bd6b4e0400e..3dbc95123a382 100644 --- a/packages/edit-widgets/src/store/reducer.js +++ b/packages/edit-widgets/src/store/reducer.js @@ -79,8 +79,21 @@ export function listViewToggleRef( state = { current: null } ) { return state; } +/** + * This reducer does nothing aside initializing a ref to the inserter sidebar toggle. + * We will have a unique ref per "editor" instance. + * + * @param {Object} state + * @return {Object} Reference to the inserter sidebar toggle button. + */ +export function inserterSidebarToggleRef( state = { current: null } ) { + return state; +} + export default combineReducers( { blockInserterPanel, + inserterSidebarToggleRef, listViewPanel, + listViewToggleRef, widgetAreasOpenState, } ); diff --git a/packages/editor/package.json b/packages/editor/package.json index bab3a2c29107c..65fa7deae1828 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -66,6 +66,8 @@ "@wordpress/wordcount": "file:../wordcount", "clsx": "^2.1.1", "date-fns": "^3.6.0", + "deepmerge": "^4.3.0", + "is-plain-object": "^5.0.0", "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0", "remove-accents": "^0.5.0" diff --git a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js new file mode 100644 index 0000000000000..4683dd38593a5 --- /dev/null +++ b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js @@ -0,0 +1,175 @@ +/** + * WordPress dependencies + */ +import { + BlockSettingsMenuControls, + __unstableBlockSettingsMenuFirstItem as BlockSettingsMenuFirstItem, + store as blockEditorStore, + useBlockDisplayInformation, +} from '@wordpress/block-editor'; +import { store as coreStore } from '@wordpress/core-data'; +import { __experimentalText as Text, MenuItem } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +function ContentOnlySettingsMenuItems( { clientId, onClose } ) { + const { entity, onNavigateToEntityRecord } = useSelect( + ( select ) => { + const { + getBlockEditingMode, + getBlockParentsByBlockName, + getSettings, + getBlockAttributes, + } = select( blockEditorStore ); + const contentOnly = + getBlockEditingMode( clientId ) === 'contentOnly'; + if ( ! contentOnly ) { + return {}; + } + const patternParent = getBlockParentsByBlockName( + clientId, + 'core/block', + true + )[ 0 ]; + + let record; + if ( patternParent ) { + record = select( coreStore ).getEntityRecord( + 'postType', + 'wp_block', + getBlockAttributes( patternParent ).ref + ); + } else { + const { getCurrentPostType, getCurrentTemplateId } = + select( editorStore ); + const currentPostType = getCurrentPostType(); + const templateId = getCurrentTemplateId(); + if ( currentPostType === 'page' && templateId ) { + record = select( coreStore ).getEntityRecord( + 'postType', + 'wp_template', + templateId + ); + } + } + return { + entity: record, + onNavigateToEntityRecord: + getSettings().onNavigateToEntityRecord, + }; + }, + [ clientId ] + ); + + if ( ! entity ) { + return ( + <TemplateLockContentOnlyMenuItems + clientId={ clientId } + onClose={ onClose } + /> + ); + } + + const isPattern = entity.type === 'wp_block'; + + return ( + <> + <BlockSettingsMenuFirstItem> + <MenuItem + onClick={ () => { + onNavigateToEntityRecord( { + postId: entity.id, + postType: entity.type, + } ); + } } + > + { isPattern ? __( 'Edit pattern' ) : __( 'Edit template' ) } + </MenuItem> + </BlockSettingsMenuFirstItem> + <Text + variant="muted" + as="p" + className="editor-content-only-settings-menu__description" + > + { isPattern + ? __( + 'Edit the pattern to move, delete, or make further changes to this block.' + ) + : __( + 'Edit the template to move, delete, or make further changes to this block.' + ) } + </Text> + </> + ); +} + +function TemplateLockContentOnlyMenuItems( { clientId, onClose } ) { + const { contentLockingParent } = useSelect( + ( select ) => { + const { getContentLockingParent } = unlock( + select( blockEditorStore ) + ); + return { + contentLockingParent: getContentLockingParent( clientId ), + }; + }, + [ clientId ] + ); + const blockDisplayInformation = + useBlockDisplayInformation( contentLockingParent ); + // Disable reason: We're using a hook here so it has to be on top-level. + // eslint-disable-next-line @wordpress/no-unused-vars-before-return + const { modifyContentLockBlock, selectBlock } = unlock( + useDispatch( blockEditorStore ) + ); + + if ( ! blockDisplayInformation?.title ) { + return null; + } + + return ( + <> + <BlockSettingsMenuFirstItem> + <MenuItem + onClick={ () => { + selectBlock( contentLockingParent ); + modifyContentLockBlock( contentLockingParent ); + onClose(); + } } + > + { __( 'Unlock' ) } + </MenuItem> + </BlockSettingsMenuFirstItem> + <Text + variant="muted" + as="p" + className="editor-content-only-settings-menu__description" + > + { __( + 'Temporarily unlock the parent block to edit, delete or make further changes to this block.' + ) } + </Text> + </> + ); +} + +export default function ContentOnlySettingsMenu() { + return ( + <BlockSettingsMenuControls> + { ( { selectedClientIds, onClose } ) => + selectedClientIds.length === 1 && ( + <ContentOnlySettingsMenuItems + clientId={ selectedClientIds[ 0 ] } + onClose={ onClose } + /> + ) + } + </BlockSettingsMenuControls> + ); +} diff --git a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.native.js b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.native.js new file mode 100644 index 0000000000000..11cebce87bbba --- /dev/null +++ b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.native.js @@ -0,0 +1,4 @@ +// Render nothing in native for now. +export default function ContentOnlySettingsMenu() { + return null; +} diff --git a/packages/editor/src/components/block-settings-menu/style.scss b/packages/editor/src/components/block-settings-menu/style.scss new file mode 100644 index 0000000000000..53fa391d28ef0 --- /dev/null +++ b/packages/editor/src/components/block-settings-menu/style.scss @@ -0,0 +1,4 @@ +.editor-content-only-settings-menu__description { + padding: $grid-unit; + min-width: 235px; +} diff --git a/packages/editor/src/components/document-tools/index.js b/packages/editor/src/components/document-tools/index.js index 64d94cc2e9165..6952fa34e31ae 100644 --- a/packages/editor/src/components/document-tools/index.js +++ b/packages/editor/src/components/document-tools/index.js @@ -16,7 +16,7 @@ import { } from '@wordpress/block-editor'; import { Button, ToolbarItem } from '@wordpress/components'; import { listView, plus } from '@wordpress/icons'; -import { useRef, useCallback } from '@wordpress/element'; +import { useCallback } from '@wordpress/element'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -35,11 +35,9 @@ const preventDefault = ( event ) => { function DocumentTools( { className, disableBlockTools = false, - children, // This is a temporary prop until the list view is fully unified between post and site editors. listViewLabel = __( 'Document Overview' ), } ) { - const inserterButton = useRef(); const { setIsInserterOpened, setIsListViewOpened } = useDispatch( editorStore ); const { @@ -47,16 +45,21 @@ function DocumentTools( { isInserterOpened, isListViewOpen, listViewShortcut, + inserterSidebarToggleRef, listViewToggleRef, hasFixedToolbar, showIconLabels, } = useSelect( ( select ) => { const { getSettings } = select( blockEditorStore ); const { get } = select( preferencesStore ); - const { isListViewOpened, getListViewToggleRef } = unlock( - select( editorStore ) - ); + const { + isListViewOpened, + getEditorMode, + getInserterSidebarToggleRef, + getListViewToggleRef, + } = unlock( select( editorStore ) ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); + const { __unstableGetEditorMode } = select( blockEditorStore ); return { isInserterOpened: select( editorStore ).isInserterOpened(), @@ -64,10 +67,13 @@ function DocumentTools( { listViewShortcut: getShortcutRepresentation( 'core/editor/toggle-list-view' ), + inserterSidebarToggleRef: getInserterSidebarToggleRef(), listViewToggleRef: getListViewToggleRef(), hasFixedToolbar: getSettings().hasFixedToolbar, showIconLabels: get( 'core', 'showIconLabels' ), isDistractionFree: get( 'core', 'distractionFree' ), + isVisualMode: getEditorMode() === 'visual', + isZoomedOutView: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); @@ -82,17 +88,10 @@ function DocumentTools( { [ setIsListViewOpened, isListViewOpen ] ); - const toggleInserter = useCallback( () => { - if ( isInserterOpened ) { - // Focusing the inserter button should close the inserter popover. - // However, there are some cases it won't close when the focus is lost. - // See https://github.com/WordPress/gutenberg/issues/43090 for more details. - inserterButton.current.focus(); - setIsInserterOpened( false ); - } else { - setIsInserterOpened( true ); - } - }, [ isInserterOpened, setIsInserterOpened ] ); + const toggleInserter = useCallback( + () => setIsInserterOpened( ! isInserterOpened ), + [ isInserterOpened, setIsInserterOpened ] + ); /* translators: button label text should, if possible, be under 16 characters. */ const longLabel = _x( @@ -118,7 +117,7 @@ function DocumentTools( { <div className="editor-document-tools__left"> { ! isDistractionFree && ( <ToolbarItem - ref={ inserterButton } + ref={ inserterSidebarToggleRef } as={ Button } className="editor-document-tools__inserter-toggle" variant="primary" @@ -179,7 +178,6 @@ function DocumentTools( { ) } </> ) } - { children } </div> </NavigableToolbar> ); diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index 8f63ee6980aac..37f03fc4a1dab 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -161,13 +161,17 @@ function EditorCanvas( { hasRootPaddingAwareAlignments, themeHasDisabledLayoutStyles, themeSupportsLayout, + isZoomOutMode, } = useSelect( ( select ) => { - const _settings = select( blockEditorStore ).getSettings(); + const { getSettings, __unstableGetEditorMode } = + select( blockEditorStore ); + const _settings = getSettings(); return { themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, themeSupportsLayout: _settings.supportsLayout, hasRootPaddingAwareAlignments: _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); @@ -319,6 +323,13 @@ function EditorCanvas( { } ), ] ); + const zoomOutProps = isZoomOutMode + ? { + scale: 'default', + frameSize: '20px', + } + : {}; + return ( <BlockCanvas shouldIframe={ @@ -332,6 +343,7 @@ function EditorCanvas( { 'has-editor-padding': showEditorPadding, } ), ...iframeProps, + ...zoomOutProps, style: { ...iframeProps?.style, ...deviceStyles, diff --git a/packages/edit-site/src/components/global-styles/global-styles-provider.js b/packages/editor/src/components/global-styles-provider/index.js similarity index 98% rename from packages/edit-site/src/components/global-styles/global-styles-provider.js rename to packages/editor/src/components/global-styles-provider/index.js index 1e2d43e267a2d..566e390b26a57 100644 --- a/packages/edit-site/src/components/global-styles/global-styles-provider.js +++ b/packages/editor/src/components/global-styles-provider/index.js @@ -7,10 +7,10 @@ import { isPlainObject } from 'is-plain-object'; /** * WordPress dependencies */ -import { useMemo, useCallback } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useMemo, useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -118,7 +118,7 @@ function useGlobalStylesBaseConfig() { return [ !! baseConfig, baseConfig ]; } -function useGlobalStylesContext() { +export function useGlobalStylesContext() { const [ isUserConfigReady, userConfig, setUserConfig ] = useGlobalStylesUserConfig(); const [ isBaseConfigReady, baseConfig ] = useGlobalStylesBaseConfig(); diff --git a/packages/editor/src/components/header/index.js b/packages/editor/src/components/header/index.js new file mode 100644 index 0000000000000..98501386434b6 --- /dev/null +++ b/packages/editor/src/components/header/index.js @@ -0,0 +1,154 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useViewportMatch } from '@wordpress/compose'; +import { __unstableMotion as motion } from '@wordpress/components'; +import { store as preferencesStore } from '@wordpress/preferences'; +import { useState } from '@wordpress/element'; +import { PinnedItems } from '@wordpress/interface'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import CollapsableBlockToolbar from '../collapsible-block-toolbar'; +import DocumentBar from '../document-bar'; +import DocumentTools from '../document-tools'; +import MoreMenu from '../more-menu'; +import PostPreviewButton from '../post-preview-button'; +import PostPublishButtonOrToggle from '../post-publish-button/post-publish-button-or-toggle'; +import PostSavedState from '../post-saved-state'; +import PostTypeSupportCheck from '../post-type-support-check'; +import PostViewLink from '../post-view-link'; +import PreviewDropdown from '../preview-dropdown'; +import { store as editorStore } from '../../store'; + +const slideY = { + hidden: { y: '-50px' }, + distractionFreeInactive: { y: 0 }, + hover: { y: 0, transition: { type: 'tween', delay: 0.2 } }, +}; + +function Header( { + customSaveButton, + forceIsDirty, + forceDisableBlockTools, + setEntitiesSavedStatesCallback, + title, + children, +} ) { + const isWideViewport = useViewportMatch( 'large' ); + const isLargeViewport = useViewportMatch( 'medium' ); + const { + isTextEditor, + isPublishSidebarOpened, + showIconLabels, + hasFixedToolbar, + isNestedEntity, + isZoomedOutView, + } = useSelect( ( select ) => { + const { get: getPreference } = select( preferencesStore ); + const { + getEditorMode, + getEditorSettings, + isPublishSidebarOpened: _isPublishSidebarOpened, + } = select( editorStore ); + const { __unstableGetEditorMode } = select( blockEditorStore ); + + return { + isTextEditor: getEditorMode() === 'text', + isPublishSidebarOpened: _isPublishSidebarOpened(), + showIconLabels: getPreference( 'core', 'showIconLabels' ), + hasFixedToolbar: getPreference( 'core', 'fixedToolbar' ), + isNestedEntity: + !! getEditorSettings().onNavigateToPreviousEntityRecord, + isZoomedOutView: __unstableGetEditorMode() === 'zoom-out', + }; + }, [] ); + + const hasTopToolbar = isLargeViewport && hasFixedToolbar; + + const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = + useState( true ); + + // The edit-post-header classname is only kept for backward compatibilty + // as some plugins might be relying on its presence. + return ( + <div className="editor-header edit-post-header"> + { children } + <motion.div + variants={ slideY } + transition={ { type: 'tween', delay: 0.8 } } + className="editor-header__toolbar" + > + <DocumentTools + disableBlockTools={ forceDisableBlockTools || isTextEditor } + /> + { hasTopToolbar && ( + <CollapsableBlockToolbar + isCollapsed={ isBlockToolsCollapsed } + onToggle={ setIsBlockToolsCollapsed } + /> + ) } + <div + className={ clsx( 'editor-header__center', { + 'is-collapsed': + ! isBlockToolsCollapsed && hasTopToolbar, + } ) } + > + { ! title ? ( + <PostTypeSupportCheck supportKeys="title"> + <DocumentBar /> + </PostTypeSupportCheck> + ) : ( + title + ) } + </div> + </motion.div> + <motion.div + variants={ slideY } + transition={ { type: 'tween', delay: 0.8 } } + className="editor-header__settings" + > + { ! customSaveButton && ! isPublishSidebarOpened && ( + // This button isn't completely hidden by the publish sidebar. + // We can't hide the whole toolbar when the publish sidebar is open because + // we want to prevent mounting/unmounting the PostPublishButtonOrToggle DOM node. + // We track that DOM node to return focus to the PostPublishButtonOrToggle + // when the publish sidebar has been closed. + <PostSavedState forceIsDirty={ forceIsDirty } /> + ) } + <PreviewDropdown + forceIsAutosaveable={ forceIsDirty } + disabled={ isNestedEntity || isZoomedOutView } + /> + <PostPreviewButton + className="editor-header__post-preview-button" + forceIsAutosaveable={ forceIsDirty } + /> + <PostViewLink /> + { ! customSaveButton && ( + <PostPublishButtonOrToggle + forceIsDirty={ forceIsDirty } + setEntitiesSavedStatesCallback={ + setEntitiesSavedStatesCallback + } + /> + ) } + { customSaveButton } + { ( isWideViewport || ! showIconLabels ) && ( + <PinnedItems.Slot scope="core" /> + ) } + <MoreMenu /> + </motion.div> + </div> + ); +} + +export default Header; diff --git a/packages/editor/src/components/header/style.scss b/packages/editor/src/components/header/style.scss new file mode 100644 index 0000000000000..3040362a7bd57 --- /dev/null +++ b/packages/editor/src/components/header/style.scss @@ -0,0 +1,231 @@ +.editor-header { + height: $header-height; + background: $white; + display: flex; + flex-wrap: wrap; + align-items: center; + // The header should never be wider than the viewport, or buttons might be hidden. Especially relevant at high zoom levels. Related to https://core.trac.wordpress.org/ticket/47603#ticket. + max-width: 100vw; + justify-content: space-between; + + // Make toolbar sticky on larger breakpoints + @include break-zoomed-in { + flex-wrap: nowrap; + } +} + +.editor-header__toolbar { + display: flex; + // Allow this area to shrink to fit the toolbar buttons. + flex-shrink: 8; + // Take up the space of the toolbar so it can be justified to the left side of the toolbar. + flex-grow: 3; + // Hide the overflow so flex will limit its width. Block toolbar will allow scrolling on fixed toolbar. + overflow: hidden; + // Leave enough room for the focus ring to show. + padding: 2px 0; + align-items: center; + // Allow focus ring to be fully visible on furthest right button. + @include break-medium() { + padding-right: var(--wp-admin-border-width-focus); + } + + .table-of-contents { + display: none; + + @include break-small() { + display: block; + } + } +} + +.editor-header__center { + flex-grow: 1; + display: flex; + justify-content: center; + + &.is-collapsed { + display: none; + } +} + +/** + * Buttons on the right side + */ + +.editor-header__settings { + display: inline-flex; + align-items: center; + flex-wrap: nowrap; + padding-right: $grid-unit-05; + + @include break-small () { + padding-right: $grid-unit-10; + } + + gap: $grid-unit-10; +} + +/** + * Show icon labels. + */ + +.show-icon-labels.interface-pinned-items, +.show-icon-labels .editor-header { + .components-button.has-icon { + width: auto; + + // Hide the button icons when labels are set to display... + svg { + display: none; + } + // ... and display labels. + &::after { + content: attr(aria-label); + } + &[aria-disabled="true"] { + background-color: transparent; + } + } + .is-tertiary { + &:active { + box-shadow: 0 0 0 1.5px var(--wp-admin-theme-color); + background-color: transparent; + } + } + // Exception for drodpdown toggle buttons. + .components-button.has-icon.button-toggle { + svg { + display: block; + } + &::after { + content: none; + } + } + + // Don't hide MenuItemsChoice check icons + .components-menu-items-choice .components-menu-items__item-icon.components-menu-items__item-icon { + display: block; + } + .editor-document-tools__inserter-toggle.editor-document-tools__inserter-toggle, + .interface-pinned-items .components-button { + padding-left: $grid-unit; + padding-right: $grid-unit; + + @include break-small { + padding-left: $grid-unit-15; + padding-right: $grid-unit-15; + } + } + + .editor-post-save-draft.editor-post-save-draft, + .editor-post-saved-state.editor-post-saved-state { + &::after { + content: none; + } + } +} + +.show-icon-labels { + .editor-header__toolbar .block-editor-block-mover { + // Modified group borders. + border-left: none; + + &::before { + content: ""; + width: $border-width; + height: $grid-unit-30; + background-color: $gray-300; + margin-top: $grid-unit-05; + margin-left: $grid-unit; + } + + // Modified block movers horizontal separator. + .block-editor-block-mover__move-button-container { + &::before { + width: calc(100% - #{$grid-unit-30}); + background: $gray-300; + left: calc(50% + 1px); + } + } + } +} + +.show-icon-labels.interface-pinned-items { + padding: 6px $grid-unit-15 $grid-unit-15; + margin-top: 0; + margin-bottom: 0; + margin-left: -$grid-unit-15; + margin-right: -$grid-unit-15; + border-bottom: 1px solid $gray-400; + display: block; + + > .components-button.has-icon { + margin: 0; + padding: 6px 6px 6px $grid-unit; + width: 14.625rem; + justify-content: flex-start; + + &[aria-expanded="true"] svg { + display: block; + max-width: $grid-unit-30; + } + &[aria-expanded="false"] { + padding-left: $grid-unit-50; + } + svg { + margin-right: 8px; + } + } +} + +.editor-header__post-preview-button { + @include break-small { + display: none; + } +} + +.is-distraction-free { + .interface-interface-skeleton__header { + border-bottom: none; + } + + .editor-header { + background-color: $white; + border-bottom: 1px solid #e0e0e0; + position: absolute; + width: 100%; + + + // hide some parts + & > .edit-post-header__settings > .edit-post-header__post-preview-button { + visibility: hidden; + } + + & > .editor-header__toolbar .editor-document-tools__document-overview-toggle, + & > .editor-header__settings > .editor-preview-dropdown, + & > .editor-header__settings > .interface-pinned-items { + display: none; + } + + } + + // We need ! important because we override inline styles + // set by the motion component. + .interface-interface-skeleton__header:focus-within { + opacity: 1 !important; + div { + transform: translateX(0) translateZ(0) !important; + } + + } + + .components-editor-notices__dismissible { + position: absolute; + z-index: 35; + } +} + +.components-popover.more-menu-dropdown__content { + z-index: z-index(".components-popover.more-menu__content"); +} diff --git a/packages/editor/src/components/inserter-sidebar/index.js b/packages/editor/src/components/inserter-sidebar/index.js index 1611ea8909683..cd45d101f187a 100644 --- a/packages/editor/src/components/inserter-sidebar/index.js +++ b/packages/editor/src/components/inserter-sidebar/index.js @@ -2,16 +2,14 @@ * WordPress dependencies */ import { useDispatch, useSelect } from '@wordpress/data'; -import { Button, VisuallyHidden } from '@wordpress/components'; -import { __experimentalLibrary as Library } from '@wordpress/block-editor'; -import { close } from '@wordpress/icons'; import { - useViewportMatch, - __experimentalUseDialog as useDialog, -} from '@wordpress/compose'; -import { __ } from '@wordpress/i18n'; -import { useRef } from '@wordpress/element'; + __experimentalLibrary as Library, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useViewportMatch } from '@wordpress/compose'; +import { useCallback, useRef } from '@wordpress/element'; import { store as preferencesStore } from '@wordpress/preferences'; +import { ESCAPE } from '@wordpress/keycodes'; /** * Internal dependencies @@ -23,52 +21,77 @@ export default function InserterSidebar( { closeGeneralSidebar, isRightSidebarOpen, } ) { - const { insertionPoint, showMostUsedBlocks } = useSelect( ( select ) => { - const { getInsertionPoint } = unlock( select( editorStore ) ); + const { + blockSectionRootClientId, + inserterSidebarToggleRef, + insertionPoint, + showMostUsedBlocks, + } = useSelect( ( select ) => { + const { getInserterSidebarToggleRef, getInsertionPoint } = unlock( + select( editorStore ) + ); + const { getBlockRootClientId, __unstableGetEditorMode, getSettings } = + select( blockEditorStore ); const { get } = select( preferencesStore ); + const getBlockSectionRootClientId = () => { + if ( __unstableGetEditorMode() === 'zoom-out' ) { + const { sectionRootClientId } = unlock( getSettings() ); + if ( sectionRootClientId ) { + return sectionRootClientId; + } + } + return getBlockRootClientId(); + }; return { + inserterSidebarToggleRef: getInserterSidebarToggleRef(), insertionPoint: getInsertionPoint(), showMostUsedBlocks: get( 'core', 'mostUsedBlocks' ), + blockSectionRootClientId: getBlockSectionRootClientId(), }; }, [] ); const { setIsInserterOpened } = useDispatch( editorStore ); const isMobileViewport = useViewportMatch( 'medium', '<' ); - const TagName = ! isMobileViewport ? VisuallyHidden : 'div'; - const [ inserterDialogRef, inserterDialogProps ] = useDialog( { - onClose: () => setIsInserterOpened( false ), - focusOnMount: true, - } ); - const libraryRef = useRef(); + // When closing the inserter, focus should return to the toggle button. + const closeInserterSidebar = useCallback( () => { + setIsInserterOpened( false ); + inserterSidebarToggleRef.current?.focus(); + }, [ inserterSidebarToggleRef, setIsInserterOpened ] ); + + const closeOnEscape = useCallback( + ( event ) => { + if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) { + event.preventDefault(); + closeInserterSidebar(); + } + }, + [ closeInserterSidebar ] + ); + return ( - <div - ref={ inserterDialogRef } - { ...inserterDialogProps } - className="editor-inserter-sidebar" - > - <TagName className="editor-inserter-sidebar__header"> - <Button - icon={ close } - label={ __( 'Close block inserter' ) } - onClick={ () => setIsInserterOpened( false ) } - /> - </TagName> + // eslint-disable-next-line jsx-a11y/no-static-element-interactions + <div onKeyDown={ closeOnEscape } className="editor-inserter-sidebar"> <div className="editor-inserter-sidebar__content"> <Library showMostUsedBlocks={ showMostUsedBlocks } showInserterHelpPanel shouldFocusBlock={ isMobileViewport } - rootClientId={ insertionPoint.rootClientId } + rootClientId={ + blockSectionRootClientId ?? insertionPoint.rootClientId + } __experimentalInsertionIndex={ insertionPoint.insertionIndex } + __experimentalInitialTab={ insertionPoint.tab } + __experimentalInitialCategory={ insertionPoint.category } __experimentalFilterValue={ insertionPoint.filterValue } __experimentalOnPatternCategorySelection={ isRightSidebarOpen ? closeGeneralSidebar : undefined } ref={ libraryRef } + onClose={ closeInserterSidebar } /> </div> </div> diff --git a/packages/editor/src/components/inserter-sidebar/style.scss b/packages/editor/src/components/inserter-sidebar/style.scss index 817c8e4d32814..e3564cbd03aaf 100644 --- a/packages/editor/src/components/inserter-sidebar/style.scss +++ b/packages/editor/src/components/inserter-sidebar/style.scss @@ -6,16 +6,22 @@ flex-direction: column; } -.editor-inserter-sidebar__header { - padding-top: $grid-unit-10; +.block-editor-inserter-sidebar__header { + border-bottom: $border-width solid $gray-300; padding-right: $grid-unit-10; display: flex; - justify-content: flex-end; + justify-content: space-between; + + .block-editor-inserter-sidebar__close-button { + order: 1; + align-self: center; + } } .editor-inserter-sidebar__content { // Leave space for the close button height: calc(100% - #{$button-size} - #{$grid-unit-10}); + @include break-medium() { height: 100%; } diff --git a/packages/editor/src/components/page-attributes/test/order.js b/packages/editor/src/components/page-attributes/test/order.js index 245cbbb8fc71d..fcb836bbfef0d 100644 --- a/packages/editor/src/components/page-attributes/test/order.js +++ b/packages/editor/src/components/page-attributes/test/order.js @@ -22,7 +22,11 @@ jest.mock( '@wordpress/data/src/components/use-dispatch', () => ( { function setupDataMock( order = 0 ) { useSelect.mockImplementation( ( mapSelect ) => mapSelect( () => ( { - getPostType: () => null, + getPostType: () => ( { + supports: { + 'page-attributes': true, + }, + } ), getEditedPostAttribute: ( attr ) => { switch ( attr ) { case 'menu_order': diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 763b354010bf1..194dd338f49e6 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -1,9 +1,9 @@ /** * WordPress dependencies */ -import { external, trash, edit, backup } from '@wordpress/icons'; +import { external, trash, backup } from '@wordpress/icons'; import { addQueryArgs } from '@wordpress/url'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { decodeEntities } from '@wordpress/html-entities'; import { store as coreStore } from '@wordpress/core-data'; import { __, _n, sprintf, _x } from '@wordpress/i18n'; @@ -21,7 +21,12 @@ import { /** * Internal dependencies */ -import { TEMPLATE_ORIGINS, TEMPLATE_POST_TYPE } from '../../store/constants'; +import { + TEMPLATE_ORIGINS, + TEMPLATE_PART_POST_TYPE, + TEMPLATE_POST_TYPE, + PATTERN_POST_TYPE, +} from '../../store/constants'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import isTemplateRevertable from '../../store/utils/is-template-revertable'; @@ -43,7 +48,13 @@ const trashPostAction = { }, supportsBulk: true, hideModalHeader: true, - RenderModal: ( { items: posts, closeModal, onActionPerformed } ) => { + RenderModal: ( { + items: posts, + closeModal, + onActionStart, + onActionPerformed, + } ) => { + const [ isBusy, setIsBusy ] = useState( false ); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); const { deleteEntityRecord } = useDispatch( coreStore ); @@ -67,12 +78,21 @@ const trashPostAction = { ) } </Text> <HStack justify="right"> - <Button variant="tertiary" onClick={ closeModal }> + <Button + variant="tertiary" + onClick={ closeModal } + disabled={ isBusy } + __experimentalIsFocusable + > { __( 'Cancel' ) } </Button> <Button variant="primary" onClick={ async () => { + setIsBusy( true ); + if ( onActionStart ) { + onActionStart( posts ); + } const promiseResult = await Promise.allSettled( posts.map( ( post ) => { return deleteEntityRecord( @@ -97,9 +117,17 @@ const trashPostAction = { __( '"%s" moved to the Trash.' ), getItemTitle( posts[ 0 ] ) ); + } else if ( posts[ 0 ].type === 'page' ) { + successMessage = sprintf( + /* translators: The number of pages. */ + __( '%s pages moved to the Trash.' ), + posts.length + ); } else { - successMessage = __( - 'Pages moved to the Trash.' + successMessage = sprintf( + /* translators: The number of posts. */ + __( '%s posts moved to the Trash.' ), + posts.length ); } createSuccessNotice( successMessage, { @@ -161,8 +189,12 @@ const trashPostAction = { if ( onActionPerformed ) { onActionPerformed( posts ); } + setIsBusy( false ); closeModal(); } } + isBusy={ isBusy } + disabled={ isBusy } + __experimentalIsFocusable > { __( 'Delete' ) } </Button> @@ -296,9 +328,9 @@ function useRestorePostAction() { return status === 'trash'; }, async callback( posts, onActionPerformed ) { - try { - for ( const post of posts ) { - await editEntityRecord( + await Promise.allSettled( + posts.map( ( post ) => { + return editEntityRecord( 'postType', post.type, post.id, @@ -306,53 +338,101 @@ function useRestorePostAction() { status: 'draft', } ); - await saveEditedEntityRecord( + } ) + ); + const promiseResult = await Promise.allSettled( + posts.map( ( post ) => { + return saveEditedEntityRecord( 'postType', post.type, post.id, { throwOnError: true } ); - } + } ) + ); - createSuccessNotice( - posts.length > 1 - ? sprintf( - /* translators: The number of posts. */ - __( '%d posts have been restored.' ), - posts.length - ) - : sprintf( - /* translators: The number of posts. */ - __( '"%s" has been restored.' ), - getItemTitle( posts[ 0 ] ) - ), - { - type: 'snackbar', - id: 'restore-post-action', - } - ); + if ( + promiseResult.every( + ( { status } ) => status === 'fulfilled' + ) + ) { + let successMessage; + if ( posts.length === 1 ) { + successMessage = sprintf( + /* translators: The number of posts. */ + __( '"%s" has been restored.' ), + getItemTitle( posts[ 0 ] ) + ); + } else if ( posts[ 0 ].type === 'page' ) { + successMessage = sprintf( + /* translators: The number of posts. */ + __( '%d pages have been restored.' ), + posts.length + ); + } else { + successMessage = sprintf( + /* translators: The number of posts. */ + __( '%d posts have been restored.' ), + posts.length + ); + } + createSuccessNotice( successMessage, { + type: 'snackbar', + id: 'restore-post-action', + } ); if ( onActionPerformed ) { onActionPerformed( posts ); } - } catch ( error ) { + } else { + // If there was at lease one failure. let errorMessage; - if ( - error.message && - error.code !== 'unknown_error' && - error.message - ) { - errorMessage = error.message; - } else if ( posts.length > 1 ) { - errorMessage = __( - 'An error occurred while restoring the posts.' - ); + // If we were trying to move a single post to the trash. + if ( promiseResult.length === 1 ) { + if ( promiseResult[ 0 ].reason?.message ) { + errorMessage = promiseResult[ 0 ].reason.message; + } else { + errorMessage = __( + 'An error occurred while restoring the post.' + ); + } + // If we were trying to move multiple posts to the trash } else { - errorMessage = __( - 'An error occurred while restoring the post.' + const errorMessages = new Set(); + const failedPromises = promiseResult.filter( + ( { status } ) => status === 'rejected' ); + for ( const failedPromise of failedPromises ) { + if ( failedPromise.reason?.message ) { + errorMessages.add( + failedPromise.reason.message + ); + } + } + if ( errorMessages.size === 0 ) { + errorMessage = __( + 'An error occurred while restoring the posts.' + ); + } else if ( errorMessages.size === 1 ) { + errorMessage = sprintf( + /* translators: %s: an error message */ + __( + 'An error occurred while restoring the posts: %s' + ), + [ ...errorMessages ][ 0 ] + ); + } else { + errorMessage = sprintf( + /* translators: %s: a list of comma separated error messages */ + __( + 'Some errors occurred while restoring the posts: %s' + ), + [ ...errorMessages ].join( ',' ) + ); + } } - - createErrorNotice( errorMessage, { type: 'snackbar' } ); + createErrorNotice( errorMessage, { + type: 'snackbar', + } ); } }, } ), @@ -382,20 +462,6 @@ const viewPostAction = { }, }; -const editPostAction = { - id: 'edit-post', - label: __( 'Edit' ), - isPrimary: true, - icon: edit, - isEligible( { status } ) { - return status !== 'trash'; - }, - callback( posts, onActionPerformed ) { - if ( onActionPerformed ) { - onActionPerformed( posts ); - } - }, -}; const postRevisionsAction = { id: 'view-post-revisions', label: __( 'View revisions' ), @@ -500,7 +566,7 @@ const renamePostAction = { }, }; -export const duplicatePostAction = { +const duplicatePostAction = { id: 'duplicate-post', label: _x( 'Duplicate', 'action label' ), isEligible( { status } ) { @@ -618,9 +684,16 @@ const resetTemplateAction = { id: 'reset-template', label: __( 'Reset' ), isEligible: isTemplateRevertable, + icon: backup, supportsBulk: true, hideModalHeader: true, - RenderModal: ( { items, closeModal, onActionPerformed } ) => { + RenderModal: ( { + items, + closeModal, + onActionStart, + onActionPerformed, + } ) => { + const [ isBusy, setIsBusy ] = useState( false ); const { revertTemplate } = unlock( useDispatch( editorStore ) ); const { saveEditedEntityRecord } = useDispatch( coreStore ); const { createSuccessNotice, createErrorNotice } = @@ -690,16 +763,29 @@ const resetTemplateAction = { { __( 'Reset to default and clear all customizations?' ) } </Text> <HStack justify="right"> - <Button variant="tertiary" onClick={ closeModal }> + <Button + variant="tertiary" + onClick={ closeModal } + disabled={ isBusy } + __experimentalIsFocusable + > { __( 'Cancel' ) } </Button> <Button variant="primary" onClick={ async () => { + setIsBusy( true ); + if ( onActionStart ) { + onActionStart( items ); + } await onConfirm( items ); onActionPerformed?.( items ); closeModal(); + isBusy( false ); } } + isBusy={ isBusy } + disabled={ isBusy } + __experimentalIsFocusable > { __( 'Reset' ) } </Button> @@ -730,9 +816,16 @@ const deleteTemplateAction = { id: 'delete-template', label: __( 'Delete' ), isEligible: isTemplateRemovable, + icon: trash, supportsBulk: true, hideModalHeader: true, - RenderModal: ( { items: templates, closeModal, onActionPerformed } ) => { + RenderModal: ( { + items: templates, + closeModal, + onActionStart, + onActionPerformed, + } ) => { + const [ isBusy, setIsBusy ] = useState( false ); const { removeTemplates } = unlock( useDispatch( editorStore ) ); return ( <VStack spacing="5"> @@ -756,18 +849,31 @@ const deleteTemplateAction = { ) } </Text> <HStack justify="right"> - <Button variant="tertiary" onClick={ closeModal }> + <Button + variant="tertiary" + onClick={ closeModal } + disabled={ isBusy } + __experimentalIsFocusable + > { __( 'Cancel' ) } </Button> <Button variant="primary" onClick={ async () => { + setIsBusy( true ); + if ( onActionStart ) { + onActionStart( templates ); + } await removeTemplates( templates, { allowUndo: false, } ); onActionPerformed?.( templates ); + setIsBusy( false ); closeModal(); } } + isBusy={ isBusy } + disabled={ isBusy } + __experimentalIsFocusable > { __( 'Delete' ) } </Button> @@ -882,96 +988,96 @@ const renameTemplateAction = { }, }; -export function usePostActions( onActionPerformed, actionIds = null ) { +export function usePostActions( postType, onActionPerformed ) { + const { postTypeObject } = useSelect( + ( select ) => { + const { getPostType } = select( coreStore ); + return { + postTypeObject: getPostType( postType ), + }; + }, + [ postType ] + ); + const permanentlyDeletePostAction = usePermanentlyDeletePostAction(); const restorePostAction = useRestorePostAction(); - return useMemo( - () => { - // By default, return all actions... - const defaultActions = [ - editPostAction, - resetTemplateAction, - viewPostAction, - restorePostAction, - deleteTemplateAction, - permanentlyDeletePostAction, - postRevisionsAction, - duplicatePostAction, - renamePostAction, - renameTemplateAction, - trashPostAction, - ]; - - // ... unless `actionIds` was specified, in which case we find the - // actions matching the given IDs. - const actions = actionIds - ? actionIds.map( ( actionId ) => - defaultActions.find( ( { id } ) => actionId === id ) - ) - : defaultActions; - - if ( onActionPerformed ) { - for ( let i = 0; i < actions.length; ++i ) { - if ( actions[ i ].callback ) { - const existingCallback = actions[ i ].callback; - actions[ i ] = { - ...actions[ i ], - callback: ( items, _onActionPerformed ) => { - existingCallback( items, ( _items ) => { - if ( _onActionPerformed ) { - _onActionPerformed( _items ); - } - onActionPerformed( - actions[ i ].id, - _items - ); - } ); - }, - }; - } - if ( actions[ i ].RenderModal ) { - const ExistingRenderModal = actions[ i ].RenderModal; - actions[ i ] = { - ...actions[ i ], - RenderModal: ( props ) => { - return ( - <ExistingRenderModal - items={ props.items } - closeModal={ props.closeModal } - onActionPerformed={ ( _items ) => { - if ( props.onActionPerformed ) { - props.onActionPerformed( - _items - ); - } - onActionPerformed( - actions[ i ].id, - _items - ); - } } - /> - ); - }, - }; - } + const isTemplateOrTemplatePart = [ + TEMPLATE_POST_TYPE, + TEMPLATE_PART_POST_TYPE, + ].includes( postType ); + const isPattern = postType === PATTERN_POST_TYPE; + const isLoaded = !! postTypeObject; + return useMemo( () => { + if ( ! isLoaded ) { + return []; + } + + const actions = [ + isTemplateOrTemplatePart && resetTemplateAction, + postTypeObject?.viewable && viewPostAction, + ! isTemplateOrTemplatePart && restorePostAction, + isTemplateOrTemplatePart && deleteTemplateAction, + ! isTemplateOrTemplatePart && permanentlyDeletePostAction, + postRevisionsAction, + process.env.IS_GUTENBERG_PLUGIN + ? ! isTemplateOrTemplatePart && + ! isPattern && + duplicatePostAction + : false, + ! isTemplateOrTemplatePart && renamePostAction, + isTemplateOrTemplatePart && renameTemplateAction, + ! isTemplateOrTemplatePart && trashPostAction, + ].filter( Boolean ); + + if ( onActionPerformed ) { + for ( let i = 0; i < actions.length; ++i ) { + if ( actions[ i ].callback ) { + const existingCallback = actions[ i ].callback; + actions[ i ] = { + ...actions[ i ], + callback: ( items, _onActionPerformed ) => { + existingCallback( items, ( _items ) => { + if ( _onActionPerformed ) { + _onActionPerformed( _items ); + } + onActionPerformed( actions[ i ].id, _items ); + } ); + }, + }; + } + if ( actions[ i ].RenderModal ) { + const ExistingRenderModal = actions[ i ].RenderModal; + actions[ i ] = { + ...actions[ i ], + RenderModal: ( props ) => { + return ( + <ExistingRenderModal + { ...props } + onActionPerformed={ ( _items ) => { + if ( props.onActionPerformed ) { + props.onActionPerformed( _items ); + } + onActionPerformed( + actions[ i ].id, + _items + ); + } } + /> + ); + }, + }; } } - return actions; - }, + } - // Disable reason: if provided, `actionIds` is a shallow array of - // strings, and the strings themselves should be part of the useMemo - // dependencies. Two different disable statements are needed, as the - // first flags what it thinks are missing dependencies, and the second - // flags the array spread operation. - // - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - // eslint-disable-next-line react-hooks/exhaustive-deps - ...( actionIds || [] ), - permanentlyDeletePostAction, - restorePostAction, - onActionPerformed, - ] - ); + return actions; + }, [ + isTemplateOrTemplatePart, + isPattern, + postTypeObject?.viewable, + permanentlyDeletePostAction, + restorePostAction, + onActionPerformed, + isLoaded, + ] ); } diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index 24d74c3f0d97f..14914ccd8f320 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -17,11 +17,6 @@ import { moreVertical } from '@wordpress/icons'; import { unlock } from '../../lock-unlock'; import { usePostActions } from './actions'; import { store as editorStore } from '../../store'; -import { - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, - PATTERN_POST_TYPE, -} from '../../store/constants'; const { DropdownMenuV2: DropdownMenu, @@ -31,36 +26,16 @@ const { kebabCase, } = unlock( componentsPrivateApis ); -let POST_ACTIONS_WHILE_EDITING = [ - 'view-post', - 'view-post-revisions', - 'rename-post', - 'move-to-trash', -]; - -if ( process.env.IS_GUTENBERG_PLUGIN ) { - POST_ACTIONS_WHILE_EDITING = [ - 'view-post', - 'view-post-revisions', - 'duplicate-post', - 'rename-post', - 'move-to-trash', - ]; -} - export default function PostActions( { onActionPerformed, buttonProps } ) { const [ isActionsMenuOpen, setIsActionsMenuOpen ] = useState( false ); - const { postType, item } = useSelect( ( select ) => { + const { item, postType } = useSelect( ( select ) => { const { getCurrentPostType, getCurrentPost } = select( editorStore ); return { - postType: getCurrentPostType(), item: getCurrentPost(), + postType: getCurrentPostType(), }; - } ); - const allActions = usePostActions( - onActionPerformed, - POST_ACTIONS_WHILE_EDITING - ); + }, [] ); + const allActions = usePostActions( postType, onActionPerformed ); const actions = useMemo( () => { return allActions.filter( ( action ) => { @@ -68,15 +43,6 @@ export default function PostActions( { onActionPerformed, buttonProps } ) { } ); }, [ allActions, item ] ); - if ( - [ - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, - PATTERN_POST_TYPE, - ].includes( postType ) - ) { - return null; - } return ( <DropdownMenu open={ isActionsMenuOpen } diff --git a/packages/edit-post/src/components/sidebar/post-format/index.js b/packages/editor/src/components/post-format/panel.js similarity index 60% rename from packages/edit-post/src/components/sidebar/post-format/index.js rename to packages/editor/src/components/post-format/panel.js index 5127fa0930f3f..cbd065183eefa 100644 --- a/packages/edit-post/src/components/sidebar/post-format/index.js +++ b/packages/editor/src/components/post-format/panel.js @@ -2,15 +2,17 @@ * WordPress dependencies */ import { PanelRow } from '@wordpress/components'; -import { - PostFormat as PostFormatForm, - PostFormatCheck, -} from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import PostFormatForm from './'; +import PostFormatCheck from './check'; export function PostFormat() { return ( <PostFormatCheck> - <PanelRow className="edit-post-post-format"> + <PanelRow className="editor-post-format__panel"> <PostFormatForm /> </PanelRow> </PostFormatCheck> diff --git a/packages/editor/src/components/post-format/style.scss b/packages/editor/src/components/post-format/style.scss index 09fb0f11b9f94..135ee7f357902 100644 --- a/packages/editor/src/components/post-format/style.scss +++ b/packages/editor/src/components/post-format/style.scss @@ -1,3 +1,9 @@ [class].editor-post-format__suggestion { margin: $grid-unit-05 0 0 0; } + +.editor-post-format__panel { + display: flex; + flex-direction: column; + align-items: stretch; +} diff --git a/packages/editor/src/components/post-last-revision/test/check.js b/packages/editor/src/components/post-last-revision/test/check.js index 6e5210e8ed6bf..0c3e680992a2e 100644 --- a/packages/editor/src/components/post-last-revision/test/check.js +++ b/packages/editor/src/components/post-last-revision/test/check.js @@ -21,7 +21,11 @@ function setupDataMock( id, count ) { getCurrentPostLastRevisionId: () => id, getCurrentPostRevisionsCount: () => count, getEditedPostAttribute: () => null, - getPostType: () => null, + getPostType: () => ( { + supports: { + revisions: true, + }, + } ), } ) ) ); } diff --git a/packages/editor/src/components/post-publish-panel/style.scss b/packages/editor/src/components/post-publish-panel/style.scss index 037980074ce0a..c0ddec29253e3 100644 --- a/packages/editor/src/components/post-publish-panel/style.scss +++ b/packages/editor/src/components/post-publish-panel/style.scss @@ -119,6 +119,11 @@ .editor-post-visibility__dialog-legend { display: none; } + + .components-panel__body-title .components-button { + align-items: flex-start; + text-wrap: pretty; + } } .post-publish-panel__postpublish .components-panel__body { diff --git a/packages/edit-post/src/components/sidebar/post-slug/index.js b/packages/editor/src/components/post-slug/panel.js similarity index 61% rename from packages/edit-post/src/components/sidebar/post-slug/index.js rename to packages/editor/src/components/post-slug/panel.js index 8b52f94bd33f5..6ab97a28b251c 100644 --- a/packages/edit-post/src/components/sidebar/post-slug/index.js +++ b/packages/editor/src/components/post-slug/panel.js @@ -2,12 +2,17 @@ * WordPress dependencies */ import { PanelRow } from '@wordpress/components'; -import { PostSlug as PostSlugForm, PostSlugCheck } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import PostSlugForm from './'; +import PostSlugCheck from './check'; export function PostSlug() { return ( <PostSlugCheck> - <PanelRow className="edit-post-post-slug"> + <PanelRow className="editor-post-slug"> <PostSlugForm /> </PanelRow> </PostSlugCheck> diff --git a/packages/edit-post/src/components/sidebar/post-slug/style.css b/packages/editor/src/components/post-slug/style.css similarity index 100% rename from packages/edit-post/src/components/sidebar/post-slug/style.css rename to packages/editor/src/components/post-slug/style.css diff --git a/packages/edit-post/src/components/sidebar/post-slug/style.css.map b/packages/editor/src/components/post-slug/style.css.map similarity index 100% rename from packages/edit-post/src/components/sidebar/post-slug/style.css.map rename to packages/editor/src/components/post-slug/style.css.map diff --git a/packages/edit-post/src/components/sidebar/post-slug/style.scss b/packages/editor/src/components/post-slug/style.scss similarity index 74% rename from packages/edit-post/src/components/sidebar/post-slug/style.scss rename to packages/editor/src/components/post-slug/style.scss index 067dfcb08d6f0..551450582128e 100644 --- a/packages/edit-post/src/components/sidebar/post-slug/style.scss +++ b/packages/editor/src/components/post-slug/style.scss @@ -1,4 +1,4 @@ -.edit-post-post-slug { +.editor-post-slug { display: flex; flex-direction: column; align-items: stretch; diff --git a/packages/editor/src/components/post-slug/test/check.js b/packages/editor/src/components/post-slug/test/check.js deleted file mode 100644 index 5b5b4ce8a440f..0000000000000 --- a/packages/editor/src/components/post-slug/test/check.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * External dependencies - */ -import { render, screen } from '@testing-library/react'; - -/** - * Internal dependencies - */ -import PostSlugCheck from '../check'; - -describe( 'PostSlugCheck', () => { - it( 'should render control', () => { - render( <PostSlugCheck>slug</PostSlugCheck> ); - - expect( screen.getByText( 'slug' ) ).toBeVisible(); - } ); -} ); diff --git a/packages/editor/src/components/post-slug/test/index.js b/packages/editor/src/components/post-slug/test/index.js index 81f254ba6363f..fb40055111b77 100644 --- a/packages/editor/src/components/post-slug/test/index.js +++ b/packages/editor/src/components/post-slug/test/index.js @@ -26,7 +26,11 @@ describe( 'PostSlug', () => { useSelect.mockImplementation( ( mapSelect ) => mapSelect( () => ( { - getPostType: () => null, + getPostType: () => ( { + supports: { + slug: true, + }, + } ), getEditedPostAttribute: () => 'post', getEditedPostSlug: () => '1', } ) ) diff --git a/packages/editor/src/components/post-sticky/panel.js b/packages/editor/src/components/post-sticky/panel.js new file mode 100644 index 0000000000000..b5ede0c1ab882 --- /dev/null +++ b/packages/editor/src/components/post-sticky/panel.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import PostPanelRow from '../post-panel-row'; +import PostStickyForm from './'; +import PostStickyCheck from './check'; + +export function PostStickyPanel() { + return ( + <PostStickyCheck> + <PostPanelRow> + <PostStickyForm /> + </PostPanelRow> + </PostStickyCheck> + ); +} + +export default PostStickyPanel; diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js b/packages/editor/src/components/post-transform-panel/hooks.js similarity index 88% rename from packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js rename to packages/editor/src/components/post-transform-panel/hooks.js index 128a74da39070..0f1ef74f0efff 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/hooks.js +++ b/packages/editor/src/components/post-transform-panel/hooks.js @@ -5,16 +5,16 @@ import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; import { parse } from '@wordpress/blocks'; +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; /** * Internal dependencies */ -import { store as editSiteStore } from '../../../store'; -import { - EXCLUDED_PATTERN_SOURCES, - PATTERN_TYPES, -} from '../../../utils/constants'; -import { unlock } from '../../../lock-unlock'; +import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; + +const { EXCLUDED_PATTERN_SOURCES, PATTERN_TYPES } = + unlock( patternsPrivateApis ); function injectThemeAttributeInBlockTemplateContent( block, @@ -67,7 +67,7 @@ function filterPatterns( patterns, template ) { } ); } -function preparePatterns( patterns, template, currentThemeStylesheet ) { +function preparePatterns( patterns, currentThemeStylesheet ) { return patterns.map( ( pattern ) => ( { ...pattern, keywords: pattern.keywords || [], @@ -86,8 +86,8 @@ function preparePatterns( patterns, template, currentThemeStylesheet ) { export function useAvailablePatterns( template ) { const { blockPatterns, restBlockPatterns, currentThemeStylesheet } = useSelect( ( select ) => { - const { getSettings } = unlock( select( editSiteStore ) ); - const settings = getSettings(); + const { getEditorSettings } = select( editorStore ); + const settings = getEditorSettings(); return { blockPatterns: diff --git a/packages/editor/src/components/post-transform-panel/index.js b/packages/editor/src/components/post-transform-panel/index.js new file mode 100644 index 0000000000000..5ed7d34909f8b --- /dev/null +++ b/packages/editor/src/components/post-transform-panel/index.js @@ -0,0 +1,99 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { PanelBody, PanelRow } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useAsyncList } from '@wordpress/compose'; +import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor'; +import { serialize } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import { useAvailablePatterns } from './hooks'; +import { + TEMPLATE_POST_TYPE, + TEMPLATE_PART_POST_TYPE, +} from '../../store/constants'; + +function TemplatesList( { availableTemplates, onSelect } ) { + const shownTemplates = useAsyncList( availableTemplates ); + if ( ! availableTemplates || availableTemplates?.length === 0 ) { + return null; + } + + return ( + <BlockPatternsList + label={ __( 'Templates' ) } + blockPatterns={ availableTemplates } + shownPatterns={ shownTemplates } + onClickPattern={ onSelect } + showTitlesAsTooltip + /> + ); +} + +function PostTransform() { + const { record, postType, postId } = useSelect( ( select ) => { + const { getCurrentPostType, getCurrentPostId } = select( editorStore ); + const { getEditedEntityRecord } = select( coreStore ); + const type = getCurrentPostType(); + const id = getCurrentPostId(); + return { + postType: type, + postId: id, + record: getEditedEntityRecord( 'postType', type, id ), + }; + }, [] ); + const { editEntityRecord } = useDispatch( coreStore ); + const availablePatterns = useAvailablePatterns( record ); + const onTemplateSelect = async ( selectedTemplate ) => { + await editEntityRecord( 'postType', postType, postId, { + blocks: selectedTemplate.blocks, + content: serialize( selectedTemplate.blocks ), + } ); + }; + if ( ! availablePatterns?.length ) { + return null; + } + + return ( + <PanelBody + title={ __( 'Transform into:' ) } + initialOpen={ record.type === TEMPLATE_PART_POST_TYPE } + > + <PanelRow> + <p> + { __( + 'Choose a predefined pattern to switch up the look of your template.' // TODO - make this dynamic? + ) } + </p> + </PanelRow> + + <TemplatesList + availableTemplates={ availablePatterns } + onSelect={ onTemplateSelect } + /> + </PanelBody> + ); +} + +export default function PostTransformPanel() { + const { postType } = useSelect( ( select ) => { + const { getCurrentPostType } = select( editorStore ); + return { + postType: getCurrentPostType(), + }; + }, [] ); + + if ( + ! [ TEMPLATE_PART_POST_TYPE, TEMPLATE_POST_TYPE ].includes( postType ) + ) { + return null; + } + + return <PostTransform />; +} diff --git a/packages/editor/src/components/post-trash/panel.js b/packages/editor/src/components/post-trash/panel.js new file mode 100644 index 0000000000000..9111c048eb60b --- /dev/null +++ b/packages/editor/src/components/post-trash/panel.js @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import PostTrashCheck from './check'; +import PostTrashLink from './'; + +export default function PostTrashPanel() { + return ( + <PostTrashCheck> + <PostTrashLink /> + </PostTrashCheck> + ); +} diff --git a/packages/editor/src/components/post-type-support-check/index.js b/packages/editor/src/components/post-type-support-check/index.js index c716593f458f1..613fda8eb82da 100644 --- a/packages/editor/src/components/post-type-support-check/index.js +++ b/packages/editor/src/components/post-type-support-check/index.js @@ -27,7 +27,7 @@ function PostTypeSupportCheck( { children, supportKeys } ) { const { getPostType } = select( coreStore ); return getPostType( getEditedPostAttribute( 'type' ) ); }, [] ); - let isSupported = true; + let isSupported = !! postType; if ( postType ) { isSupported = ( Array.isArray( supportKeys ) ? supportKeys : [ supportKeys ] diff --git a/packages/editor/src/components/post-type-support-check/test/index.js b/packages/editor/src/components/post-type-support-check/test/index.js index 8acef8abacb6b..71b9f083a74a2 100644 --- a/packages/editor/src/components/post-type-support-check/test/index.js +++ b/packages/editor/src/components/post-type-support-check/test/index.js @@ -29,7 +29,7 @@ function setupUseSelectMock( postType ) { } describe( 'PostTypeSupportCheck', () => { - it( 'renders its children when post type is not known', () => { + it( 'does not render its children when post type is not known', () => { setupUseSelectMock( undefined ); const { container } = render( @@ -38,7 +38,7 @@ describe( 'PostTypeSupportCheck', () => { </PostTypeSupportCheck> ); - expect( container ).toHaveTextContent( 'Supported' ); + expect( container ).not.toHaveTextContent( 'Supported' ); } ); it( 'does not render its children when post type is known and not supports', () => { diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index df0fb488c69dc..4f359104ea02d 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -28,6 +28,7 @@ import useCommands from '../commands'; import BlockRemovalWarnings from '../block-removal-warnings'; import StartPageOptions from '../start-page-options'; import KeyboardShortcutHelpModal from '../keyboard-shortcut-help-modal'; +import ContentOnlySettingsMenu from '../block-settings-menu/content-only-settings-menu'; const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); @@ -264,6 +265,7 @@ export const ExperimentalEditorProvider = withRegistryProvider( { ! settings.__unstableIsPreviewMode && ( <> <PatternsMenuItems /> + <ContentOnlySettingsMenu /> { mode === 'template-locked' && ( <DisableNonPageContentBlocks /> ) } diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index bcd0885614183..2a9ecf6073248 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -24,8 +24,10 @@ import inserterMediaCategories from '../media-categories'; import { mediaUpload } from '../../utils'; import { store as editorStore } from '../../store'; import { lock, unlock } from '../../lock-unlock'; +import { useGlobalStylesContext } from '../global-styles-provider'; const EMPTY_BLOCKS_LIST = []; +const DEFAULT_STYLES = {}; function __experimentalReusableBlocksSelect( select ) { return ( @@ -173,6 +175,9 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { [ postType, postId, isLargeViewport, renderingMode ] ); + const { merged: mergedGlobalStyles } = useGlobalStylesContext(); + const globalStylesData = mergedGlobalStyles.styles ?? DEFAULT_STYLES; + const settingsBlockPatterns = settings.__experimentalAdditionalBlockPatterns ?? // WP 6.0 settings.__experimentalBlockPatterns; // WP 5.9 @@ -251,6 +256,8 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { }, [ settings.allowedBlockTypes, hiddenBlockTypes, blockTypes ] ); const forceDisableFocusMode = settings.focusMode === false; + const { globalStylesDataKey, selectBlockPatternsKey } = + unlock( privateApis ); return useMemo( () => { const blockEditorSettings = { @@ -259,6 +266,7 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { BLOCK_EDITOR_SETTINGS.includes( key ) ) ), + [ globalStylesDataKey ]: globalStylesData, allowedBlockTypes, allowRightClickOverrides, focusMode: focusMode && ! forceDisableFocusMode, @@ -267,10 +275,14 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { keepCaretInsideBlock, mediaUpload: hasUploadPermissions ? mediaUpload : undefined, __experimentalBlockPatterns: blockPatterns, - [ unlock( privateApis ).selectBlockPatternsKey ]: ( select ) => - unlock( select( coreStore ) ).getBlockPatternsForPostType( - postType - ), + [ selectBlockPatternsKey ]: ( select ) => { + const { hasFinishedResolution, getBlockPatternsForPostType } = + unlock( select( coreStore ) ); + const patterns = getBlockPatternsForPostType( postType ); + return hasFinishedResolution( 'getBlockPatterns' ) + ? patterns + : undefined; + }, [ unlock( privateApis ).reusableBlocksSelectKey ]: __experimentalReusableBlocksSelect, __experimentalBlockPatternCategories: blockPatternCategories, @@ -327,6 +339,9 @@ function useBlockEditorSettings( settings, postType, postId, renderingMode ) { postType, setIsInserterOpened, sectionRootClientId, + globalStylesData, + globalStylesDataKey, + selectBlockPatternsKey, ] ); } diff --git a/packages/editor/src/components/sidebar/constants.js b/packages/editor/src/components/sidebar/constants.js new file mode 100644 index 0000000000000..be660c2169239 --- /dev/null +++ b/packages/editor/src/components/sidebar/constants.js @@ -0,0 +1,4 @@ +export const sidebars = { + document: 'edit-post/document', + block: 'edit-post/block', +}; diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/editor/src/components/sidebar/header.js similarity index 82% rename from packages/edit-post/src/components/sidebar/settings-header/index.js rename to packages/editor/src/components/sidebar/header.js index 244e21b1acd43..fc4d44ba9e295 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/editor/src/components/sidebar/header.js @@ -5,17 +5,17 @@ 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'; /** * Internal dependencies */ -import { unlock } from '../../../lock-unlock'; -import { sidebars } from '../settings-sidebar'; +import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; +import { sidebars } from './constants'; const { Tabs } = unlock( componentsPrivateApis ); -const SettingsHeader = ( _, ref ) => { +const SidebarHeader = ( _, ref ) => { const { documentLabel } = useSelect( ( select ) => { const { getPostTypeLabel } = select( editorStore ); @@ -46,4 +46,4 @@ const SettingsHeader = ( _, ref ) => { ); }; -export default forwardRef( SettingsHeader ); +export default forwardRef( SidebarHeader ); diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/editor/src/components/sidebar/index.js similarity index 60% rename from packages/edit-post/src/components/sidebar/settings-sidebar/index.js rename to packages/editor/src/components/sidebar/index.js index fd5b136ba461d..7c1f914c725c8 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/editor/src/components/sidebar/index.js @@ -13,48 +13,53 @@ import { useEffect, useRef, } from '@wordpress/element'; -import { isRTL, __, sprintf } from '@wordpress/i18n'; +import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; -import { - store as editorStore, - PageAttributesPanel, - PluginDocumentSettingPanel, - PluginSidebar, - PostDiscussionPanel, - PostLastRevisionPanel, - PostTaxonomiesPanel, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; -import { addQueryArgs } from '@wordpress/url'; -import { store as noticesStore } from '@wordpress/notices'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { store as interfaceStore } from '@wordpress/interface'; /** * Internal dependencies */ -import SettingsHeader from '../settings-header'; -import PostStatus from '../post-status'; -import MetaBoxes from '../../meta-boxes'; -import { store as editPostStore } from '../../../store'; -import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '../../../lock-unlock'; +import PageAttributesPanel from '../page-attributes/panel'; +import PatternOverridesPanel from '../pattern-overrides-panel'; +import PluginDocumentSettingPanel from '../plugin-document-setting-panel'; +import PluginSidebar from '../plugin-sidebar'; +import PostActions from '../post-actions'; +import PostCardPanel from '../post-card-panel'; +import PostDiscussionPanel from '../post-discussion/panel'; +import PostLastRevisionPanel from '../post-last-revision/panel'; +import PostSummary from './post-summary'; +import PostTaxonomiesPanel from '../post-taxonomies/panel'; +import PostTransformPanel from '../post-transform-panel'; +import SidebarHeader from './header'; +import TemplateContentPanel from '../template-content-panel'; +import useAutoSwitchEditorSidebars from '../provider/use-auto-switch-editor-sidebars'; +import { sidebars } from './constants'; +import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; +import { + NAVIGATION_POST_TYPE, + TEMPLATE_PART_POST_TYPE, + TEMPLATE_POST_TYPE, +} from '../../store/constants'; -const { PostCardPanel, PostActions, interfaceStore } = - unlock( editorPrivateApis ); const { Tabs } = unlock( componentsPrivateApis ); -const { PatternOverridesPanel, useAutoSwitchEditorSidebars } = - unlock( editorPrivateApis ); const SIDEBAR_ACTIVE_BY_DEFAULT = Platform.select( { web: true, native: false, } ); -export const sidebars = { - document: 'edit-post/document', - block: 'edit-post/block', -}; -const SidebarContent = ( { tabName, keyboardShortcut, isEditingTemplate } ) => { +const SidebarContent = ( { + tabName, + keyboardShortcut, + renderingMode, + onActionPerformed, + extraPanels, + showSummary = true, +} ) => { const tabListRef = useRef( null ); // Because `PluginSidebar` renders a `ComplementaryArea`, we // need to forward the `Tabs` context so it can be passed through the @@ -87,71 +92,21 @@ const SidebarContent = ( { tabName, keyboardShortcut, isEditingTemplate } ) => { selectedTabElement?.focus(); } }, [ tabName ] ); - const { createSuccessNotice } = useDispatch( noticesStore ); - - const onActionPerformed = useCallback( - ( actionId, items ) => { - switch ( actionId ) { - case 'move-to-trash': - { - const postType = items[ 0 ].type; - document.location.href = addQueryArgs( 'edit.php', { - post_type: postType, - } ); - } - break; - case 'duplicate-post': - { - const newItem = items[ 0 ]; - const title = - typeof newItem.title === 'string' - ? newItem.title - : newItem.title?.rendered; - createSuccessNotice( - sprintf( - // translators: %s: Title of the created post e.g: "Post 1". - __( '"%s" successfully created.' ), - title - ), - { - type: 'snackbar', - id: 'duplicate-post-action', - actions: [ - { - label: __( 'Edit' ), - onClick: () => { - const postId = newItem.id; - document.location.href = - addQueryArgs( 'post.php', { - post: postId, - action: 'edit', - } ); - }, - }, - ], - } - ); - } - break; - } - }, - [ createSuccessNotice ] - ); return ( <PluginSidebar identifier={ tabName } header={ <Tabs.Context.Provider value={ tabsContextValue }> - <SettingsHeader ref={ tabListRef } /> + <SidebarHeader ref={ tabListRef } /> </Tabs.Context.Provider> } closeLabel={ __( 'Close Settings' ) } // This classname is added so we can apply a corrective negative // margin to the panel. // see https://github.com/WordPress/gutenberg/pull/55360#pullrequestreview-1737671049 - className="edit-post-sidebar__panel" - headerClassName="edit-post-sidebar__panel-tabs" + className="editor-sidebar__panel" + headerClassName="editor-sidebar__panel-tabs" /* translators: button label text should, if possible, be under 16 characters. */ title={ __( 'Settings' ) } toggleShortcut={ keyboardShortcut } @@ -167,14 +122,18 @@ const SidebarContent = ( { tabName, keyboardShortcut, isEditingTemplate } ) => { /> } /> - { ! isEditingTemplate && <PostStatus /> } + { showSummary && <PostSummary /> } <PluginDocumentSettingPanel.Slot /> + { renderingMode !== 'post-only' && ( + <TemplateContentPanel /> + ) } + <PostTransformPanel /> <PostLastRevisionPanel /> <PostTaxonomiesPanel /> <PostDiscussionPanel /> <PageAttributesPanel /> <PatternOverridesPanel /> - { ! isEditingTemplate && <MetaBoxes location="side" /> } + { extraPanels } </Tabs.TabPanel> <Tabs.TabPanel tabId={ sidebars.block } focusable={ false }> <BlockInspector /> @@ -184,9 +143,9 @@ const SidebarContent = ( { tabName, keyboardShortcut, isEditingTemplate } ) => { ); }; -const SettingsSidebar = () => { +const Sidebar = ( { extraPanels, onActionPerformed } ) => { useAutoSwitchEditorSidebars(); - const { tabName, keyboardShortcut, isEditingTemplate } = useSelect( + const { tabName, keyboardShortcut, showSummary, renderingMode } = useSelect( ( select ) => { const shortcut = select( keyboardShortcutsStore @@ -210,23 +169,26 @@ const SettingsSidebar = () => { return { tabName: _tabName, keyboardShortcut: shortcut, - isEditingTemplate: - select( editorStore ).getCurrentPostType() === - 'wp_template', + showSummary: ! [ + TEMPLATE_POST_TYPE, + TEMPLATE_PART_POST_TYPE, + NAVIGATION_POST_TYPE, + ].includes( select( editorStore ).getCurrentPostType() ), + renderingMode: select( editorStore ).getRenderingMode(), }; }, [] ); - const { openGeneralSidebar } = useDispatch( editPostStore ); + const { enableComplementaryArea } = useDispatch( interfaceStore ); const onTabSelect = useCallback( ( newSelectedTabId ) => { if ( !! newSelectedTabId ) { - openGeneralSidebar( newSelectedTabId ); + enableComplementaryArea( 'core', newSelectedTabId ); } }, - [ openGeneralSidebar ] + [ enableComplementaryArea ] ); return ( @@ -238,10 +200,13 @@ const SettingsSidebar = () => { <SidebarContent tabName={ tabName } keyboardShortcut={ keyboardShortcut } - isEditingTemplate={ isEditingTemplate } + showSummary={ showSummary } + renderingMode={ renderingMode } + onActionPerformed={ onActionPerformed } + extraPanels={ extraPanels } /> </Tabs> ); }; -export default SettingsSidebar; +export default Sidebar; diff --git a/packages/editor/src/components/sidebar/post-summary.js b/packages/editor/src/components/sidebar/post-summary.js new file mode 100644 index 0000000000000..8c05eafeba9da --- /dev/null +++ b/packages/editor/src/components/sidebar/post-summary.js @@ -0,0 +1,116 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, + PanelBody, +} from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import PluginPostStatusInfo from '../plugin-post-status-info'; +import PostAuthorPanel from '../post-author/panel'; +import PostContentInformation from '../post-content-information'; +import { PrivatePostExcerptPanel as PostExcerptPanel } from '../post-excerpt/panel'; +import PostFeaturedImagePanel from '../post-featured-image/panel'; +import PostFormatPanel from '../post-format/panel'; +import PostLastEditedPanel from '../post-last-edited-panel'; +import PostSchedulePanel from '../post-schedule/panel'; +import PostSlugPanel from '../post-slug/panel'; +import PostStatusPanel from '../post-status'; +import PostStickyPanel from '../post-sticky'; +import PostSyncStatus from '../post-sync-status'; +import PostTemplatePanel from '../post-template/panel'; +import PostTrashPanel from '../post-trash/panel'; +import PostURLPanel from '../post-url/panel'; +import { store as editorStore } from '../../store'; +import { PATTERN_POST_TYPE } from '../../store/constants'; + +/** + * Module Constants + */ +const PANEL_NAME = 'post-status'; + +export default function PostSummary() { + const { isOpened, isRemoved, isPattern } = useSelect( ( select ) => { + // We use isEditorPanelRemoved to hide the panel if it was programatically removed. We do + // not use isEditorPanelEnabled since this panel should not be disabled through the UI. + const { + isEditorPanelRemoved, + isEditorPanelOpened, + getCurrentPostType, + } = select( editorStore ); + const postType = getCurrentPostType(); + return { + isRemoved: isEditorPanelRemoved( PANEL_NAME ), + isOpened: isEditorPanelOpened( PANEL_NAME ), + // Post excerpt panel is rendered in different place depending on the post type. + // So we cannot make this check inside the PostExcerpt component based on the current edited entity. + isPattern: postType === PATTERN_POST_TYPE, + }; + }, [] ); + const { toggleEditorPanelOpened } = useDispatch( editorStore ); + + if ( isRemoved ) { + return null; + } + + return ( + <PanelBody + title={ __( 'Summary' ) } + opened={ isOpened } + onToggle={ () => toggleEditorPanelOpened( PANEL_NAME ) } + > + <PluginPostStatusInfo.Slot> + { ( fills ) => ( + <> + { ! isPattern && ( + <VStack + spacing={ 3 } + // TODO: this needs to be consolidated with the panel in site editor, when we unify them. + style={ { marginBlockEnd: '24px' } } + > + <PostFeaturedImagePanel + withPanelBody={ false } + /> + <PostExcerptPanel /> + <VStack spacing={ 1 }> + <PostContentInformation /> + <PostLastEditedPanel /> + </VStack> + </VStack> + ) } + <VStack + spacing={ 1 } + style={ { marginBlockEnd: '12px' } } + > + <PostStatusPanel /> + <PostSchedulePanel /> + <PostTemplatePanel /> + <PostURLPanel /> + <PostSyncStatus /> + </VStack> + <PostStickyPanel /> + <PostFormatPanel /> + <PostSlugPanel /> + <PostAuthorPanel /> + { fills } + { ! isPattern && ( + <HStack + style={ { + marginTop: '16px', + } } + > + <PostTrashPanel /> + </HStack> + ) } + </> + ) } + </PluginPostStatusInfo.Slot> + </PanelBody> + ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/style.scss b/packages/editor/src/components/sidebar/style.scss similarity index 74% rename from packages/edit-site/src/components/sidebar-edit-mode/settings-header/style.scss rename to packages/editor/src/components/sidebar/style.scss index d74432451e1d4..000f4c6123766 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/style.scss +++ b/packages/editor/src/components/sidebar/style.scss @@ -1,4 +1,4 @@ -.components-panel__header.edit-site-sidebar-edit-mode__panel-tabs { +.components-panel__header.editor-sidebar__panel-tabs { padding-left: 0; padding-right: $grid-unit-20; diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js b/packages/editor/src/components/template-content-panel/index.js similarity index 64% rename from packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js rename to packages/editor/src/components/template-content-panel/index.js index 18a742add41b2..e0f131f27feb5 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-content.js +++ b/packages/editor/src/components/template-content-panel/index.js @@ -6,11 +6,13 @@ import { store as blockEditorStore, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; +import { PanelBody } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { unlock } from '../../../lock-unlock'; +import { unlock } from '../../lock-unlock'; const { BlockQuickNavigation } = unlock( blockEditorPrivateApis ); @@ -20,10 +22,15 @@ const PAGE_CONTENT_BLOCKS = [ 'core/post-title', ]; -export default function PageContent() { +export default function TemplateContentPanel() { const clientIds = useSelect( ( select ) => { const { getBlocksByName } = select( blockEditorStore ); return getBlocksByName( PAGE_CONTENT_BLOCKS ); }, [] ); - return <BlockQuickNavigation clientIds={ clientIds } />; + + return ( + <PanelBody title={ __( 'Content' ) }> + <BlockQuickNavigation clientIds={ clientIds } /> + </PanelBody> + ); } diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index 762c6bec89c88..41414743f9a6b 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -6,22 +6,18 @@ import * as interfaceApis from '@wordpress/interface'; /** * Internal dependencies */ -import CollapsableBlockToolbar from './components/collapsible-block-toolbar'; import EditorCanvas from './components/editor-canvas'; import { ExperimentalEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; import { EntitiesSavedStatesExtensible } from './components/entities-saved-states'; import useAutoSwitchEditorSidebars from './components/provider/use-auto-switch-editor-sidebars'; import useBlockEditorSettings from './components/provider/use-block-editor-settings'; -import DocumentTools from './components/document-tools'; +import Header from './components/header'; import InserterSidebar from './components/inserter-sidebar'; import ListViewSidebar from './components/list-view-sidebar'; -import MoreMenu from './components/more-menu'; import PatternOverridesPanel from './components/pattern-overrides-panel'; import PluginPostExcerpt from './components/post-excerpt/plugin'; import PostPanelRow from './components/post-panel-row'; -import PostViewLink from './components/post-view-link'; -import PreviewDropdown from './components/preview-dropdown'; import PreferencesModal from './components/preferences-modal'; import PostActions from './components/post-actions'; import { usePostActions } from './components/post-actions/actions'; @@ -30,29 +26,31 @@ import PostStatus from './components/post-status'; import ToolsMoreMenuGroup from './components/more-menu/tools-more-menu-group'; import ViewMoreMenuGroup from './components/more-menu/view-more-menu-group'; import { PrivatePostExcerptPanel } from './components/post-excerpt/panel'; -import PostPublishButtonOrToggle from './components/post-publish-button/post-publish-button-or-toggle'; import SavePublishPanels from './components/save-publish-panels'; import PostContentInformation from './components/post-content-information'; import PostLastEditedPanel from './components/post-last-edited-panel'; +import Sidebar from './components/sidebar'; +import { + mergeBaseAndUserConfigs, + GlobalStylesProvider, +} from './components/global-styles-provider'; const { store: interfaceStore, ...remainingInterfaceApis } = interfaceApis; export const privateApis = {}; lock( privateApis, { - CollapsableBlockToolbar, - DocumentTools, EditorCanvas, ExperimentalEditorProvider, EntitiesSavedStatesExtensible, + GlobalStylesProvider, + Header, InserterSidebar, ListViewSidebar, - MoreMenu, + mergeBaseAndUserConfigs, PatternOverridesPanel, PluginPostExcerpt, PostActions, PostPanelRow, - PostViewLink, - PreviewDropdown, PreferencesModal, usePostActions, PostCardPanel, @@ -60,10 +58,10 @@ lock( privateApis, { ToolsMoreMenuGroup, ViewMoreMenuGroup, PrivatePostExcerptPanel, - PostPublishButtonOrToggle, SavePublishPanels, PostContentInformation, PostLastEditedPanel, + Sidebar, // This is a temporary private API while we're updating the site editor to use EditorProvider. useAutoSwitchEditorSidebars, diff --git a/packages/editor/src/private-apis.native.js b/packages/editor/src/private-apis.native.js new file mode 100644 index 0000000000000..78ef82c327f8f --- /dev/null +++ b/packages/editor/src/private-apis.native.js @@ -0,0 +1,61 @@ +/** + * WordPress dependencies + */ +import * as interfaceApis from '@wordpress/interface'; + +/** + * Internal dependencies + */ +import EditorCanvas from './components/editor-canvas'; +import { ExperimentalEditorProvider } from './components/provider'; +import { lock } from './lock-unlock'; +import { EntitiesSavedStatesExtensible } from './components/entities-saved-states'; +import useAutoSwitchEditorSidebars from './components/provider/use-auto-switch-editor-sidebars'; +import useBlockEditorSettings from './components/provider/use-block-editor-settings'; +import InserterSidebar from './components/inserter-sidebar'; +import ListViewSidebar from './components/list-view-sidebar'; +import PatternOverridesPanel from './components/pattern-overrides-panel'; +import PluginPostExcerpt from './components/post-excerpt/plugin'; +import PostPanelRow from './components/post-panel-row'; +import PreferencesModal from './components/preferences-modal'; +import PostActions from './components/post-actions'; +import { usePostActions } from './components/post-actions/actions'; +import PostCardPanel from './components/post-card-panel'; +import PostStatus from './components/post-status'; +import ToolsMoreMenuGroup from './components/more-menu/tools-more-menu-group'; +import ViewMoreMenuGroup from './components/more-menu/view-more-menu-group'; +import { PrivatePostExcerptPanel } from './components/post-excerpt/panel'; +import SavePublishPanels from './components/save-publish-panels'; +import PostContentInformation from './components/post-content-information'; +import PostLastEditedPanel from './components/post-last-edited-panel'; + +const { store: interfaceStore, ...remainingInterfaceApis } = interfaceApis; + +export const privateApis = {}; +lock( privateApis, { + EditorCanvas, + ExperimentalEditorProvider, + EntitiesSavedStatesExtensible, + InserterSidebar, + ListViewSidebar, + PatternOverridesPanel, + PluginPostExcerpt, + PostActions, + PostPanelRow, + PreferencesModal, + usePostActions, + PostCardPanel, + PostStatus, + ToolsMoreMenuGroup, + ViewMoreMenuGroup, + PrivatePostExcerptPanel, + SavePublishPanels, + PostContentInformation, + PostLastEditedPanel, + + // This is a temporary private API while we're updating the site editor to use EditorProvider. + useAutoSwitchEditorSidebars, + useBlockEditorSettings, + interfaceStore, + ...remainingInterfaceApis, +} ); diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js index 5abd72f13713b..aa2af9172ff18 100644 --- a/packages/editor/src/store/private-selectors.js +++ b/packages/editor/src/store/private-selectors.js @@ -75,6 +75,9 @@ export const getInsertionPoint = createRegistrySelector( ( select ) => export function getListViewToggleRef( state ) { return state.listViewToggleRef; } +export function getInserterSidebarToggleRef( state ) { + return state.inserterSidebarToggleRef; +} const CARD_ICONS = { wp_block: symbol, wp_navigation: navigation, diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 202baa1e7e5cb..f9b4e05ffa8e5 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -360,6 +360,17 @@ export function listViewToggleRef( state = { current: null } ) { return state; } +/** + * This reducer does nothing aside initializing a ref to the inserter sidebar toggle. + * We will have a unique ref per "editor" instance. + * + * @param {Object} state + * @return {Object} Reference to the inserter sidebar toggle button. + */ +export function inserterSidebarToggleRef( state = { current: null } ) { + return state; +} + export function publishSidebarActive( state = false, action ) { switch ( action.type ) { case 'OPEN_PUBLISH_SIDEBAR': @@ -387,6 +398,7 @@ export default combineReducers( { deviceType, removedPanels, blockInserterPanel, + inserterSidebarToggleRef, listViewPanel, listViewToggleRef, publishSidebarActive, diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index ed61638ffb7ef..dcc693c08b80a 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -3,12 +3,14 @@ @import "./components/autocompleters/style.scss"; @import "./components/block-manager/style.scss"; @import "./components/collapsible-block-toolbar/style.scss"; +@import "./components/block-settings-menu/style.scss"; @import "./components/document-bar/style.scss"; @import "./components/document-outline/style.scss"; @import "./components/document-tools/style.scss"; @import "./components/editor-notices/style.scss"; @import "./components/entities-saved-states/style.scss"; @import "./components/error-boundary/style.scss"; +@import "./components/header/style.scss"; @import "./components/inserter-sidebar/style.scss"; @import "./components/keyboard-shortcut-help-modal/style.scss"; @import "./components/list-view-sidebar/style.scss"; @@ -26,6 +28,7 @@ @import "./components/post-publish-panel/style.scss"; @import "./components/post-saved-state/style.scss"; @import "./components/post-schedule/style.scss"; +@import "./components/post-slug/style.scss"; @import "./components/post-status/style.scss"; @import "./components/post-sync-status/style.scss"; @import "./components/post-taxonomies/style.scss"; @@ -38,5 +41,6 @@ @import "./components/preview-dropdown/style.scss"; @import "./components/save-publish-panels/style.scss"; @import "./components/start-page-options/style.scss"; +@import "./components/sidebar/style.scss"; @import "./components/table-of-contents/style.scss"; @import "./components/template-areas/style.scss"; diff --git a/packages/element/package.json b/packages/element/package.json index 153e817837ce4..18abd14b0e0a5 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -29,8 +29,8 @@ "sideEffects": false, "dependencies": { "@babel/runtime": "^7.16.0", - "@types/react": "^18.0.21", - "@types/react-dom": "^18.0.6", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", "@wordpress/escape-html": "file:../escape-html", "change-case": "^4.1.2", "is-plain-object": "^5.0.0", diff --git a/packages/eslint-plugin/configs/react.js b/packages/eslint-plugin/configs/react.js index 3562b0e70074b..04e4341c720be 100644 --- a/packages/eslint-plugin/configs/react.js +++ b/packages/eslint-plugin/configs/react.js @@ -37,7 +37,7 @@ module.exports = { 'react-hooks/exhaustive-deps': [ 'warn', { - additionalHooks: '(useSelect|useSuspenseSelect)', + additionalHooks: '^(useSelect|useSuspenseSelect)$', }, ], 'react-hooks/rules-of-hooks': 'error', diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 72b38ed4a4ea4..56352eb4c489b 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Bug Fixes + +- Allow multiple event handlers for the same type with `data-wp-on-document` and `data-wp-on-window`. ([#61009](https://github.com/WordPress/gutenberg/pull/61009)) + +- Prevent wrong written directives from killing the runtime ([#61249](https://github.com/WordPress/gutenberg/pull/61249)) + ## 5.6.0 (2024-05-02) ## 5.5.0 (2024-04-19) diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js index 7afcbbbd60e22..c09cecb4afc12 100644 --- a/packages/interactivity/src/directives.js +++ b/packages/interactivity/src/directives.js @@ -205,21 +205,21 @@ const cssStringToObject = ( val ) => { * @param {string} type 'window' or 'document' * @return {void} */ -const getGlobalEventDirective = - ( type ) => - ( { directives, evaluate } ) => { +const getGlobalEventDirective = ( type ) => { + return ( { directives, evaluate } ) => { directives[ `on-${ type }` ] .filter( ( { suffix } ) => suffix !== 'default' ) .forEach( ( entry ) => { + const eventName = entry.suffix.split( '--', 1 )[ 0 ]; useInit( () => { const cb = ( event ) => evaluate( entry, event ); const globalVar = type === 'window' ? window : document; - globalVar.addEventListener( entry.suffix, cb ); - return () => - globalVar.removeEventListener( entry.suffix, cb ); + globalVar.addEventListener( eventName, cb ); + return () => globalVar.removeEventListener( eventName, cb ); }, [] ); } ); }; +}; export default () => { // data-wp-context diff --git a/packages/interactivity/src/vdom.ts b/packages/interactivity/src/vdom.ts index 05b49012c4886..9e6221bb871f4 100644 --- a/packages/interactivity/src/vdom.ts +++ b/packages/interactivity/src/vdom.ts @@ -118,11 +118,23 @@ export function toVdom( root ) { if ( directives.length ) { props.__directives = directives.reduce( ( obj, [ name, ns, value ] ) => { - const [ , prefix, suffix = 'default' ] = - directiveParser.exec( name ); - if ( ! obj[ prefix ] ) { - obj[ prefix ] = []; + const directiveMatch = directiveParser.exec( name ); + if ( directiveMatch === null ) { + if ( + // @ts-expect-error This is a debug-only warning. + typeof SCRIPT_DEBUG !== 'undefined' && + // @ts-expect-error This is a debug-only warning. + SCRIPT_DEBUG === true + ) { + // eslint-disable-next-line no-console + console.warn( `Invalid directive: ${ name }.` ); + } + return obj; } + const prefix = directiveMatch[ 1 ] || ''; + const suffix = directiveMatch[ 2 ] || 'default'; + + obj[ prefix ] = obj[ prefix ] || []; obj[ prefix ].push( { namespace: ns ?? currentNamespace(), value, diff --git a/packages/interface/src/components/complementary-area-header/style.scss b/packages/interface/src/components/complementary-area-header/style.scss index 20fbe881d5694..a6b9b53206960 100644 --- a/packages/interface/src/components/complementary-area-header/style.scss +++ b/packages/interface/src/components/complementary-area-header/style.scss @@ -17,7 +17,8 @@ .interface-complementary-area-header { background: $white; - padding-right: $grid-unit-05; + padding-right: $grid-unit-15; // Reduced padding to account for close buttons. + gap: $grid-unit-10; // Always ensure space between contents and close buttons. .interface-complementary-area-header__title { margin: 0; @@ -36,11 +37,3 @@ } } } - -// This overrides the negative margins between two consecutives panels. -// since the first panel is hidden. -.components-panel__header + .interface-complementary-area-header { - @include break-medium() { - margin-top: 0; - } -} diff --git a/packages/interface/src/components/complementary-area/index.js b/packages/interface/src/components/complementary-area/index.js index ca80ae75e2e2f..b37b8b7da33d0 100644 --- a/packages/interface/src/components/complementary-area/index.js +++ b/packages/interface/src/components/complementary-area/index.js @@ -302,6 +302,7 @@ function ComplementaryArea( { smallScreenTitle={ smallScreenTitle } toggleButtonProps={ { label: closeLabel, + size: 'small', shortcut: toggleShortcut, scope, identifier, @@ -329,6 +330,7 @@ function ComplementaryArea( { } isPressed={ isPinned } aria-expanded={ isPinned } + size="compact" /> ) } </> diff --git a/packages/interface/src/components/complementary-area/style.scss b/packages/interface/src/components/complementary-area/style.scss index 143911c43ecc5..c15be5678a446 100644 --- a/packages/interface/src/components/complementary-area/style.scss +++ b/packages/interface/src/components/complementary-area/style.scss @@ -24,7 +24,7 @@ top: 0; z-index: z-index(".interface-complementary-area .components-panel__header"); - &.edit-post-sidebar__panel-tabs { + &.editor-sidebar__panel-tabs { top: $panel-header-height; @include break-medium() { @@ -39,6 +39,7 @@ h2 { font-size: $default-font-size; + font-weight: 500; color: $gray-900; margin-bottom: 1.5em; } diff --git a/packages/interface/src/components/interface-skeleton/style.scss b/packages/interface/src/components/interface-skeleton/style.scss index a140b73be55b9..a0e56658355ac 100644 --- a/packages/interface/src/components/interface-skeleton/style.scss +++ b/packages/interface/src/components/interface-skeleton/style.scss @@ -5,8 +5,10 @@ html.interface-interface-skeleton__html-container { width: 100%; @include break-medium() { - position: initial; - width: initial; + &:not(:has(.is-zoom-out)) { + position: initial; + width: initial; + } } } diff --git a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap index 77e32d991fa4d..2fefa70d5b0a2 100644 --- a/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap +++ b/packages/nux/src/components/dot-tip/test/__snapshots__/index.js.snap @@ -7,7 +7,7 @@ exports[`DotTip should render correctly 1`] = ` data-wp-c16t="true" data-wp-component="Popover" role="dialog" - style="position: absolute; top: 0px; left: 0px; opacity: 1; transform: none; transform-origin: 0% 50% 0;" + style="position: absolute; top: 0px; left: 0px; opacity: 1; transform: translateX(0px) translateY(0px) translateX(0em) scale(1) translateZ(0); transform-origin: 0% 50% 0;" tabindex="-1" > <div diff --git a/packages/patterns/src/components/patterns-manage-button.js b/packages/patterns/src/components/patterns-manage-button.js index f2bd798a7b6fa..bab9cab11462a 100644 --- a/packages/patterns/src/components/patterns-manage-button.js +++ b/packages/patterns/src/components/patterns-manage-button.js @@ -37,7 +37,7 @@ function PatternsManageButton( { clientId } ) { // The site editor and templates both check whether the user // has edit_theme_options capabilities. We can leverage that here // and omit the manage patterns link if the user can't access it. - managePatternsUrl: canUser( 'read', 'templates' ) + managePatternsUrl: canUser( 'create', 'templates' ) ? addQueryArgs( 'site-editor.php', { path: '/patterns', } ) diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css index ce39beede024f..484cdfebfbd9b 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css @@ -64,17 +64,17 @@ } /* Remove tabs from sidebar panel, leaving the \'x\' button */ -.edit-post-sidebar__panel-tabs { +.editor-sidebar__panel-tabs { display: none; } /* Remove \'(no-title)\' string from sidebar header */ -.edit-post-sidebar-header__title { +.editor-sidebar-header__title { display: none; } /* Move \'x\' close button to the end on sidebar header */ -.edit-post-sidebar-header__small { +.editor-sidebar-header__small { justify-content: flex-end; } diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js index a535eade291a0..c0138517400fb 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js @@ -36,7 +36,7 @@ function ReusableBlocksManageButton( { clientId } ) { // The site editor and templates both check whether the user // has edit_theme_options capabilities. We can leverage that here // and omit the manage patterns link if the user can't access it. - managePatternsUrl: canUser( 'read', 'templates' ) + managePatternsUrl: canUser( 'create', 'templates' ) ? addQueryArgs( 'site-editor.php', { path: '/patterns', } ) diff --git a/packages/router/src/history.js b/packages/router/src/history.js index df0e2b219cfce..56c85914a5453 100644 --- a/packages/router/src/history.js +++ b/packages/router/src/history.js @@ -13,13 +13,28 @@ const history = createBrowserHistory(); const originalHistoryPush = history.push; const originalHistoryReplace = history.replace; +// Preserve the `wp_theme_preview` query parameter when navigating +// around the Site Editor. +// TODO: move this hack out of the router into Site Editor code. +function preserveThemePreview( params ) { + if ( params.hasOwnProperty( 'wp_theme_preview' ) ) { + return params; + } + const currentSearch = new URLSearchParams( history.location.search ); + const currentThemePreview = currentSearch.get( 'wp_theme_preview' ); + if ( currentThemePreview === null ) { + return params; + } + return { ...params, wp_theme_preview: currentThemePreview }; +} + function push( params, state ) { - const search = buildQueryString( params ); + const search = buildQueryString( preserveThemePreview( params ) ); return originalHistoryPush.call( history, { search }, state ); } function replace( params, state ) { - const search = buildQueryString( params ); + const search = buildQueryString( preserveThemePreview( params ) ); return originalHistoryReplace.call( history, { search }, state ); } diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 36c3450f4e8c6..b5070d0a71b6b 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Add RTL support when building CSS styles with `build` and `start` scripts ([#61540](https://github.com/WordPress/gutenberg/pull/61540)). + ## 27.8.0 (2024-05-02) ## 27.7.0 (2024-04-19) @@ -334,7 +338,7 @@ ### New Features - Added a new `plugin-zip` command to create a zip file for a WordPress plugin ([#37687](https://github.com/WordPress/gutenberg/pull/37687)). -- Added optional support for React Fast Refresh in the `start` command. It can be activated with `--hot` CLI argument ([#28273](https://github.com/WordPress/gutenberg/pull/28273)). For now, it requires that WordPress has the [`SCRIPT_DEBUG`](https://wordpress.org/documentation/article/debugging-in-wordpress/#script_debug) flag enabled and the [Gutenberg](https://wordpress.org/plugins/gutenberg/) plugin installed. +- Added optional support for React Fast Refresh in the `start` command. It can be activated with `--hot` CLI argument ([#28273](https://github.com/WordPress/gutenberg/pull/28273)). For now, it requires that WordPress has the [`SCRIPT_DEBUG`](https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/#script_debug) flag enabled and the [Gutenberg](https://wordpress.org/plugins/gutenberg/) plugin installed. - Automatically copy `block.json` files located in the `src` folder and its subfolders to the output folder (`build` by default) ([#37612](https://github.com/WordPress/gutenberg/pull/37612)). - Scan the `src` directory for `block.json` files to detect defined scripts to use them as entry points with the `start` and `build` commands. ([#37661](https://github.com/WordPress/gutenberg/pull/37661)). diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 4a3c037ce78f9..cb15c7bf8e0ce 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -387,7 +387,7 @@ This is how you execute the script with presented setup: This script automatically use the optimized config but sometimes you may want to specify some custom options: -- `--hot` – enables "Fast Refresh". The page will automatically reload if you make changes to the code. _For now, it requires that WordPress has the [`SCRIPT_DEBUG`](https://wordpress.org/documentation/article/debugging-in-wordpress/#script_debug) flag enabled and the [Gutenberg](https://wordpress.org/plugins/gutenberg/) plugin installed._ +- `--hot` – enables "Fast Refresh". The page will automatically reload if you make changes to the code. _For now, it requires that WordPress has the [`SCRIPT_DEBUG`](https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/#script_debug) flag enabled and the [Gutenberg](https://wordpress.org/plugins/gutenberg/) plugin installed._ - `--no-watch` – Starts the build for development without starting the watcher. - `--webpack-bundle-analyzer` – enables visualization for the size of webpack output files with an interactive zoomable treemap. - `--webpack-copy-php` – enables copying all PHP files from the source directory ( default is `src` ) and its subfolders to the output directory. diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js index 386cc1be49d40..b7f61eb215eaf 100644 --- a/packages/scripts/config/webpack.config.js +++ b/packages/scripts/config/webpack.config.js @@ -9,6 +9,7 @@ const browserslist = require( 'browserslist' ); const MiniCSSExtractPlugin = require( 'mini-css-extract-plugin' ); const { basename, dirname, resolve } = require( 'path' ); const ReactRefreshWebpackPlugin = require( '@pmmmwh/react-refresh-webpack-plugin' ); +const RtlCssPlugin = require( 'rtlcss-webpack-plugin' ); const TerserPlugin = require( 'terser-webpack-plugin' ); const { realpathSync } = require( 'fs' ); const { sync: glob } = require( 'fast-glob' ); @@ -382,6 +383,10 @@ const scriptConfig = { process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(), // MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript. new MiniCSSExtractPlugin( { filename: '[name].css' } ), + // RtlCssPlugin to generate RTL CSS files. + new RtlCssPlugin( { + filename: `[name]-rtl.css`, + } ), // React Fast Refresh. hasReactFastRefresh && new ReactRefreshWebpackPlugin(), // WP_NO_EXTERNALS global variable controls whether scripts' assets get diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 4a3f319525a72..d5cdd85940dd4 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -79,6 +79,7 @@ "react-refresh": "^0.14.0", "read-pkg-up": "^7.0.1", "resolve-bin": "^0.4.0", + "rtlcss-webpack-plugin": "^4.0.7", "sass": "^1.35.2", "sass-loader": "^12.1.0", "source-map-loader": "^3.0.0", diff --git a/packages/style-engine/src/styles/background/index.ts b/packages/style-engine/src/styles/background/index.ts index a8c8679888e15..6e79636cfda12 100644 --- a/packages/style-engine/src/styles/background/index.ts +++ b/packages/style-engine/src/styles/background/index.ts @@ -39,7 +39,7 @@ const backgroundImage = { }; const backgroundPosition = { - name: 'backgroundRepeat', + name: 'backgroundPosition', generate: ( style: Style, options: StyleOptions ) => { return generateRule( style, diff --git a/packages/widgets/src/blocks/legacy-widget/index.php b/packages/widgets/src/blocks/legacy-widget/index.php index 24ea288be3875..ee13ae9a1c612 100644 --- a/packages/widgets/src/blocks/legacy-widget/index.php +++ b/packages/widgets/src/blocks/legacy-widget/index.php @@ -8,6 +8,8 @@ /** * Renders the 'core/legacy-widget' block. * + * @since 5.8.0 + * * @global int $wp_widget_factory. * * @param array $attributes The block attributes. @@ -56,6 +58,8 @@ function render_block_core_legacy_widget( $attributes ) { /** * Registers the 'core/legacy-widget' block. + * + * @since 5.8.0 */ function register_block_core_legacy_widget() { register_block_type_from_metadata( @@ -72,6 +76,8 @@ function register_block_core_legacy_widget() { * Intercepts any request with legacy-widget-preview in the query param and, if * set, renders a page containing a preview of the requested Legacy Widget * block. + * + * @since 5.8.0 */ function handle_legacy_widget_preview_iframe() { if ( empty( $_GET['legacy-widget-preview'] ) ) { diff --git a/packages/widgets/src/blocks/widget-group/index.php b/packages/widgets/src/blocks/widget-group/index.php index 284ca66a85ce7..e8769612a2f17 100644 --- a/packages/widgets/src/blocks/widget-group/index.php +++ b/packages/widgets/src/blocks/widget-group/index.php @@ -8,6 +8,8 @@ /** * Renders the 'core/widget-group' block. * + * @since 5.9.0 + * * @global array $wp_registered_sidebars * @global int|string $_sidebar_being_rendered * @@ -45,6 +47,8 @@ function render_block_core_widget_group( $attributes, $content, $block ) { /** * Registers the 'core/widget-group' block. + * + * @since 5.9.0 */ function register_block_core_widget_group() { register_block_type_from_metadata( @@ -62,6 +66,8 @@ function register_block_core_widget_group() { * it. This lets us get to the current sidebar in * render_block_core_widget_group(). * + * @since 5.9.0 + * * @global int|string $_sidebar_being_rendered * * @param int|string $index Index, name, or ID of the dynamic sidebar. @@ -76,6 +82,8 @@ function note_sidebar_being_rendered( $index ) { * Clear whatever we set in note_sidebar_being_rendered() after WordPress * finishes rendering a sidebar. * + * @since 5.9.0 + * * @global int|string $_sidebar_being_rendered */ function discard_sidebar_being_rendered() { diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000000000..8149c96950057 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,31 @@ +# Dependency patches + +Sometimes there are problems with dependencies that can be solved by patching them. Gutenberg uses +[`patch-package`](https://www.npmjs.com/package/patch-package) to patch npm dependencies when +they're installed. + +Existing patches should be described and justified here. + +## Patches + +### `patches/lighthouse+10.4.0.patch` + +No notes. + +### `patches/react-autosize-textarea+7.1.0.patch` + +This package is unmaintained. It's incompatible with some recent versions of React types in ways +that are mostly harmless. + +The `onPointerEnterCapture` and `onPointerLeaveCapture` events were removed. The package is patched +to remove those events as well. + +See https://github.com/facebook/react/pull/17883. + +### `patches/react-devtools-core+4.28.5.patch` + +No notes. + +### `patches/react-native+0.73.3.patch` + +No notes. diff --git a/patches/react-autosize-textarea+7.1.0.patch b/patches/react-autosize-textarea+7.1.0.patch new file mode 100644 index 0000000000000..7d67e58fd5fab --- /dev/null +++ b/patches/react-autosize-textarea+7.1.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-autosize-textarea/lib/TextareaAutosize.d.ts b/node_modules/react-autosize-textarea/lib/TextareaAutosize.d.ts +index ad6ff3f..e0a6d51 100644 +--- a/node_modules/react-autosize-textarea/lib/TextareaAutosize.d.ts ++++ b/node_modules/react-autosize-textarea/lib/TextareaAutosize.d.ts +@@ -22,7 +22,7 @@ export declare namespace TextareaAutosize { + lineHeight: number | null; + }; + } +-export declare const TextareaAutosize: React.ForwardRefExoticComponent<Pick<React.HTMLProps<HTMLTextAreaElement>, "default" | "max" | "required" | "media" | "hidden" | "cite" | "data" | "dir" | "form" | "label" | "slot" | "span" | "style" | "title" | "pattern" | "async" | "start" | "low" | "high" | "defer" | "open" | "disabled" | "color" | "content" | "size" | "wrap" | "multiple" | "summary" | "className" | "height" | "id" | "lang" | "method" | "min" | "name" | "target" | "type" | "width" | "role" | "tabIndex" | "crossOrigin" | "href" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "children" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "key" | "classID" | "useMap" | "wmode" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "draggable" | "placeholder" | "spellCheck" | "translate" | "radioGroup" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "kind" | "src" | "srcLang" | "value" | "download" | "hrefLang" | "rel" | "alt" | "coords" | "shape" | "autoPlay" | "controls" | "loop" | "mediaGroup" | "muted" | "playsInline" | "preload" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | "dateTime" | "acceptCharset" | "action" | "autoComplete" | "encType" | "noValidate" | "manifest" | "allowFullScreen" | "allowTransparency" | "frameBorder" | "marginHeight" | "marginWidth" | "sandbox" | "scrolling" | "seamless" | "srcDoc" | "sizes" | "srcSet" | "accept" | "capture" | "checked" | "list" | "maxLength" | "minLength" | "readOnly" | "step" | "challenge" | "keyType" | "keyParams" | "htmlFor" | "as" | "integrity" | "charSet" | "httpEquiv" | "optimum" | "reversed" | "selected" | "nonce" | "scoped" | "cellPadding" | "cellSpacing" | "colSpan" | "headers" | "rowSpan" | "scope" | "cols" | "rows" | "poster"> & { ++export declare const TextareaAutosize: React.ForwardRefExoticComponent<Pick<React.HTMLProps<HTMLTextAreaElement>, "default" | "max" | "required" | "media" | "hidden" | "cite" | "data" | "dir" | "form" | "label" | "slot" | "span" | "style" | "title" | "pattern" | "async" | "start" | "low" | "high" | "defer" | "open" | "disabled" | "color" | "content" | "size" | "wrap" | "multiple" | "summary" | "className" | "height" | "id" | "lang" | "method" | "min" | "name" | "target" | "type" | "width" | "role" | "tabIndex" | "crossOrigin" | "href" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-hidden" | "aria-invalid" | "aria-keyshortcuts" | "aria-label" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "children" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerLeave" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "key" | "classID" | "useMap" | "wmode" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "contentEditable" | "contextMenu" | "draggable" | "placeholder" | "spellCheck" | "translate" | "radioGroup" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "kind" | "src" | "srcLang" | "value" | "download" | "hrefLang" | "rel" | "alt" | "coords" | "shape" | "autoPlay" | "controls" | "loop" | "mediaGroup" | "muted" | "playsInline" | "preload" | "autoFocus" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | "dateTime" | "acceptCharset" | "action" | "autoComplete" | "encType" | "noValidate" | "manifest" | "allowFullScreen" | "allowTransparency" | "frameBorder" | "marginHeight" | "marginWidth" | "sandbox" | "scrolling" | "seamless" | "srcDoc" | "sizes" | "srcSet" | "accept" | "capture" | "checked" | "list" | "maxLength" | "minLength" | "readOnly" | "step" | "challenge" | "keyType" | "keyParams" | "htmlFor" | "as" | "integrity" | "charSet" | "httpEquiv" | "optimum" | "reversed" | "selected" | "nonce" | "scoped" | "cellPadding" | "cellSpacing" | "colSpan" | "headers" | "rowSpan" | "scope" | "cols" | "rows" | "poster"> & { + /** Called whenever the textarea resizes */ + onResize?: ((e: Event) => void) | undefined; + /** Minimum number of visible rows */ diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 400b4d68ece44..45d742a498c65 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -172,8 +172,11 @@ </property> </properties> </rule> - <rule ref="Gutenberg.Commenting.FunctionCommentSinceTag"> + <rule ref="Gutenberg.Commenting.SinceTag"> <!-- The sniff ensures that functions have a valid @since tag but skips checking experimental blocks. --> <include-pattern>/packages/block-library/src/.+/*\.php$</include-pattern> + <include-pattern>/packages/block-serialization-default-parser/.+/*\.php$</include-pattern> + <include-pattern>/packages/widgets/src/blocks/legacy-widget/index\.php$</include-pattern> + <include-pattern>/packages/widgets/src/blocks/widget-group/index\.php$</include-pattern> </rule> </ruleset> diff --git a/phpunit/blocks/register-block-style-test.php b/phpunit/blocks/register-block-style-test.php new file mode 100644 index 0000000000000..1becaff347e5f --- /dev/null +++ b/phpunit/blocks/register-block-style-test.php @@ -0,0 +1,32 @@ +<?php +/** + * Tests for `gutenberg_register_block_style`. + * + * @package gutenberg + */ + +/** + * Tests for Gutenberg's extended block style registration function. + * + * @group blocks + */ +class Tests_Blocks_Register_Block_Style extends WP_UnitTestCase { + /** + * Tests `gutenberg_register_block_style` registers block style + * across multiple block types. + */ + public function test_gutenberg_register_block_style() { + $block_types = array( 'core/group', 'core/columns', 'core/cover' ); + $style_properties = array( + 'name' => 'fancy', + 'label' => 'Fancy', + ); + + gutenberg_register_block_style( $block_types, $style_properties ); + $registry = WP_Block_Styles_Registry::get_instance(); + + $this->assertTrue( $registry->is_registered( 'core/group', 'fancy' ) ); + $this->assertTrue( $registry->is_registered( 'core/columns', 'fancy' ) ); + $this->assertTrue( $registry->is_registered( 'core/cover', 'fancy' ) ); + } +} diff --git a/post-content.php b/post-content.php index 0bdeb30733f6c..3c9b18087b0e8 100644 --- a/post-content.php +++ b/post-content.php @@ -61,7 +61,7 @@ <!-- /wp:paragraph --> <!-- wp:list --> -<ul> +<ul class="wp-block-list"> <li><?php _e( 'Text &amp; Headings', 'gutenberg' ); ?></li> <li><?php _e( 'Images &amp; Videos', 'gutenberg' ); ?></li> <li><?php _e( 'Galleries', 'gutenberg' ); ?></li> diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 979e8697b3a88..46f0c671fdd65 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -2300,7 +2300,6 @@ }, "background": {}, "color": {}, - "dimensions": {}, "layout": {}, "lightbox": {}, "spacing": {}, diff --git a/storybook/manager-head.html b/storybook/manager-head.html index 9515e4cbf2f5f..08df7dfdb7257 100644 --- a/storybook/manager-head.html +++ b/storybook/manager-head.html @@ -3,6 +3,7 @@ const PREVIOUSLY_EXPERIMENTAL_COMPONENTS = [ 'navigation', 'customselectcontrol-v2', + 'theme', ]; const REDIRECTS = [ { diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index 314834816388b..f556cb973642e 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -424,6 +424,9 @@ test.describe( 'Image', () => { page, editor, } ) => { + // This is a temp workaround for dragging and dropping images from the inserter. + // This should be removed when we have the zoom out view for media categories. + await page.setViewportSize( { width: 1400, height: 800 } ); await editor.insertBlock( { name: 'core/image' } ); const imageBlock = editor.canvas.getByRole( 'document', { name: 'Block: Image', @@ -842,552 +845,97 @@ test.describe( 'Image', () => { } ); } ); -// Skipping these tests for now as we plan -// to update them to use the new lightbox syntax -// once it's merged -- see the following PRs -// https://github.com/WordPress/gutenberg/pull/53851 -// https://github.com/WordPress/gutenberg/pull/54071 -test.describe.skip( 'Image - interactivity', () => { +test.describe( 'Image - lightbox', () => { + let uploadedMedia; + test.beforeAll( async ( { requestUtils } ) => { await requestUtils.deleteAllMedia(); + uploadedMedia = await requestUtils.uploadMedia( + path.resolve( + process.cwd(), + 'test/e2e/assets/10x10_e2e_test_image_z9T8jK.png' + ) + ); } ); test.afterAll( async ( { requestUtils } ) => { await requestUtils.deleteAllMedia(); } ); - test.beforeEach( async ( { admin, editor } ) => { + test.beforeEach( async ( { admin } ) => { await admin.createNewPost(); - await editor.insertBlock( { name: 'core/image' } ); } ); - test.afterEach( async ( { requestUtils } ) => { - await requestUtils.deleteAllMedia(); - } ); - - test.describe( 'tests using uploaded image', () => { - let filename = null; - - test( 'should toggle "lightbox" in saved attributes', async ( { - editor, - page, - imageBlockUtils, - } ) => { - const imageBlock = editor.canvas.locator( - 'role=document[name="Block: Image"i]' - ); - await expect( imageBlock ).toBeVisible(); - - filename = await imageBlockUtils.upload( - imageBlock.locator( 'data-testid=form-file-upload-input' ) - ); - const image = imageBlock.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( - 'src', - new RegExp( filename ) - ); - - await editor.openDocumentSettingsSidebar(); - - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - await page - .getByRole( 'combobox', { name: 'Behaviors' } ) - .selectOption( 'lightbox' ); - - let blocks = await editor.getBlocks(); - expect( blocks[ 0 ].attributes ).toMatchObject( { - behaviors: { - lightbox: { - animation: 'zoom', - enabled: true, - }, - }, - linkDestination: 'none', - } ); - expect( blocks[ 0 ].attributes.url ).toContain( filename ); - - await page.getByLabel( 'Behaviors' ).selectOption( '' ); - blocks = await editor.getBlocks(); - expect( blocks[ 0 ].attributes ).toMatchObject( { - behaviors: { - lightbox: { - animation: '', - enabled: false, - }, - }, - linkDestination: 'none', - } ); - expect( blocks[ 0 ].attributes.url ).toContain( filename ); - } ); - - test.describe( 'should open and close the image in a lightbox when using a mouse and dynamically load src', () => { - test( 'zoom animation', async ( { - editor, - page, - imageBlockUtils, - } ) => { - const imageBlock = editor.canvas.locator( - 'role=document[name="Block: Image"i]' - ); - await expect( imageBlock ).toBeVisible(); - - filename = await imageBlockUtils.upload( - imageBlock.locator( 'data-testid=form-file-upload-input' ), - '3200x2400_e2e_test_image_responsive_lightbox.jpeg' - ); - const image = imageBlock.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( - 'src', - new RegExp( filename ), - { timeout: 10_000 } - ); - - await editor.openDocumentSettingsSidebar(); - - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - await page - .getByRole( 'combobox', { name: 'Behaviors' } ) - .selectOption( 'lightbox' ); - - await page - .getByRole( 'combobox', { name: 'Animation' } ) - .selectOption( 'zoom' ); - - const postId = await editor.publishPost(); - await page.goto( `/?p=${ postId }` ); - - // getByRole() doesn't work for the image here for - // some reason, so let's use locators instead - const contentFigure = page.locator( '.entry-content figure' ); - const contentImage = page.locator( - '.entry-content figure img' - ); - - const wpContext = - await contentFigure.getAttribute( 'data-wp-context' ); - - const imageUploadedSrc = - JSON.parse( wpContext ).core.image.imageUploadedSrc; - - const contentImageCurrentSrc = await contentImage.evaluate( - ( img ) => img.currentSrc - ); - - const lightbox = page.locator( '.wp-lightbox-overlay' ); - await expect( lightbox ).toBeHidden(); - const responsiveImage = lightbox.locator( - '.responsive-image img' - ); - const enlargedImage = lightbox.locator( '.enlarged-image img' ); - - await expect( responsiveImage ).toHaveAttribute( - 'src', - contentImageCurrentSrc - ); - await expect( enlargedImage ).toHaveAttribute( 'src', '' ); - - await page - .getByRole( 'button', { name: 'Enlarge image' } ) - .click(); - - await expect( responsiveImage ).toHaveAttribute( - 'src', - contentImageCurrentSrc + test.describe( 'should respect theme.json settings and block overrides', () => { + test.describe( 'Theme.json settings - allow editing FALSE, enabled FALSE', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'lightbox-allow-editing-false-enabled-false' ); - await expect( enlargedImage ).toHaveAttribute( - 'src', - imageUploadedSrc - ); - - await expect( lightbox ).toBeVisible(); - - // Use page.evaluate to get the content of the style tag - const styleTagContent = await page.evaluate( () => { - const styleTag = document.querySelector( - 'style#wp-lightbox-styles' - ); - return styleTag ? styleTag.textContent : ''; - } ); - - // Define the keys you want to check for - const keysToCheck = [ - '--wp--lightbox-initial-top-position', - '--wp--lightbox-initial-left-position', - '--wp--lightbox-container-width', - '--wp--lightbox-container-height', - '--wp--lightbox-image-width', - '--wp--lightbox-image-height', - '--wp--lightbox-scale', - ]; - - // Check if all the keys are present in the style tag's content - const keysPresent = keysToCheck.every( ( key ) => - styleTagContent.includes( key ) - ); - - expect( keysPresent ).toBe( true ); - - const closeButton = lightbox.getByRole( 'button', { - name: 'Close', - } ); - await closeButton.click(); - - await expect( responsiveImage ).toHaveAttribute( - 'src', - contentImageCurrentSrc - ); - await expect( enlargedImage ).toHaveAttribute( - 'src', - imageUploadedSrc - ); - - await expect( lightbox ).toBeHidden(); - } ); - } ); - - test( 'lightbox should be overriden when link is configured for image', async ( { - editor, - page, - imageBlockUtils, - } ) => { - const imageBlock = editor.canvas.locator( - 'role=document[name="Block: Image"i]' - ); - await expect( imageBlock ).toBeVisible(); - - filename = await imageBlockUtils.upload( - imageBlock.locator( 'data-testid=form-file-upload-input' ) - ); - const image = imageBlock.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( - 'src', - new RegExp( filename ) - ); - - await editor.openDocumentSettingsSidebar(); - - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - const behaviorSelect = page.getByRole( 'combobox', { - name: 'Behaviors', } ); - await behaviorSelect.selectOption( 'lightbox' ); - - await page - .getByLabel( 'Block tools' ) - .getByLabel( 'Insert link' ) - .click(); - - const form = page.locator( - '.block-editor-url-popover__link-editor' - ); - - const url = 'https://wordpress.org'; - await form.getByLabel( 'URL' ).fill( url ); - - await form.getByRole( 'button', { name: 'Apply' } ).click(); - await expect( behaviorSelect ).toBeDisabled(); - - const postId = await editor.publishPost(); - await page.goto( `/?p=${ postId }` ); - - // The lightbox markup should not appear in the DOM at all - await expect( - page.getByRole( 'button', { name: 'Enlarge image' } ) - ).not.toBeInViewport(); - } ); - - test( 'markup should not appear if Lightbox is disabled', async ( { - editor, - page, - imageBlockUtils, - } ) => { - const imageBlock = editor.canvas.locator( - 'role=document[name="Block: Image"i]' - ); - await expect( imageBlock ).toBeVisible(); - - filename = await imageBlockUtils.upload( - imageBlock.locator( 'data-testid=form-file-upload-input' ) - ); - const image = imageBlock.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( - 'src', - new RegExp( filename ) - ); - - await editor.openDocumentSettingsSidebar(); - - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - const behaviorSelect = page.getByRole( 'combobox', { - name: 'Behaviors', + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'lightbox-allow-editing-false-enabled-false' + ); } ); - await behaviorSelect.selectOption( '' ); - - const postId = await editor.publishPost(); - await page.goto( `/?p=${ postId }` ); - - // The lightbox markup should not appear in the DOM at all - await expect( - page.getByRole( 'button', { name: 'Enlarge image' } ) - ).not.toBeInViewport(); - } ); - test.describe( 'Animation Select visibility', () => { - test( 'Animation selector should appear if Behavior is Lightbox', async ( { + test( 'Block settings - link DISABLED, lightbox UNDEFINED - should hide UI when block override is undefined', async ( { editor, page, - imageBlockUtils, } ) => { - const imageBlock = editor.canvas.locator( - 'role=document[name="Block: Image"i]' - ); - await expect( imageBlock ).toBeVisible(); - - filename = await imageBlockUtils.upload( - imageBlock.locator( 'data-testid=form-file-upload-input' ) - ); - const image = imageBlock.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( - 'src', - new RegExp( filename ) - ); + await editor.setContent( `<!-- wp:image {"id":${ uploadedMedia.id },"sizeSlug":"full","linkDestination":"none"} --> + <figure class="wp-block-image size-full"><img src="${ uploadedMedia.source_url }" alt="" class="wp-image-${ uploadedMedia.id }"/></figure> + <!-- /wp:image --> ` ); - await editor.openDocumentSettingsSidebar(); - - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - const behaviorSelect = page.getByRole( 'combobox', { - name: 'Behaviors', - } ); - await behaviorSelect.selectOption( 'lightbox' ); - await expect( - page.getByRole( 'combobox', { - name: 'Animation', - } ) - ).toBeVisible(); - } ); - test( 'Animation selector should NOT appear if Behavior is None', async ( { - page, - editor, - imageBlockUtils, - } ) => { const imageBlock = editor.canvas.locator( 'role=document[name="Block: Image"i]' ); - await expect( imageBlock ).toBeVisible(); - filename = await imageBlockUtils.upload( - imageBlock.locator( 'data-testid=form-file-upload-input' ) - ); - const image = imageBlock.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( - 'src', - new RegExp( filename ) - ); - - await editor.openDocumentSettingsSidebar(); + await imageBlock.click(); + await page + .getByLabel( 'Block tools' ) + .getByLabel( 'Link' ) + .click(); - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - const behaviorSelect = page.getByRole( 'combobox', { - name: 'Behaviors', - } ); - await behaviorSelect.selectOption( '' ); await expect( - page.getByRole( 'combobox', { - name: 'Animation', + page.getByRole( 'menuitem', { + name: 'Expand on click', } ) ).toBeHidden(); } ); - test( 'Animation selector should NOT appear if Behavior is Default', async ( { - page, + + test( 'Block settings - link DISABLED, lightbox ENABLED - should show UI while block override is active, but hide UI if override is removed', async ( { editor, - imageBlockUtils, + page, } ) => { + await editor.setContent( `<!-- wp:image {"id":${ uploadedMedia.id },"sizeSlug":"full","linkDestination":"none","lightbox":{"enabled":true}} --> + <figure class="wp-block-image size-full"><img src="${ uploadedMedia.source_url }" alt="" class="wp-image-${ uploadedMedia.id }"/></figure> + <!-- /wp:image --> ` ); + const imageBlock = editor.canvas.locator( 'role=document[name="Block: Image"i]' ); - await expect( imageBlock ).toBeVisible(); - filename = await imageBlockUtils.upload( - imageBlock.locator( 'data-testid=form-file-upload-input' ) - ); - const image = imageBlock.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( - 'src', - new RegExp( filename ) - ); + await imageBlock.click(); + await page + .getByLabel( 'Block tools' ) + .getByLabel( 'Link' ) + .click(); - await editor.openDocumentSettingsSidebar(); + await page + .getByRole( 'button', { + name: 'Disable expand on click', + } ) + .click(); - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - const behaviorSelect = page.getByRole( 'combobox', { - name: 'Behaviors', - } ); - await behaviorSelect.selectOption( 'default' ); await expect( - page.getByRole( 'combobox', { - name: 'Animation', + page.getByRole( 'menuitem', { + name: 'Expand on click', } ) ).toBeHidden(); } ); } ); - - test.describe( 'keyboard navigation', () => { - let openLightboxButton; - let lightbox; - let closeButton; - - test.beforeEach( async ( { page, editor, imageBlockUtils } ) => { - const imageBlock = editor.canvas.locator( - 'role=document[name="Block: Image"i]' - ); - await expect( imageBlock ).toBeVisible(); - - filename = await imageBlockUtils.upload( - imageBlock.locator( 'data-testid=form-file-upload-input' ) - ); - const image = imageBlock.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( - 'src', - new RegExp( filename ) - ); - - await editor.openDocumentSettingsSidebar(); - - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - await page - .getByRole( 'combobox', { name: 'Behaviors' } ) - .selectOption( 'lightbox' ); - - const postId = await editor.publishPost(); - await page.goto( `/?p=${ postId }` ); - - openLightboxButton = page.getByRole( 'button', { - name: 'Enlarge image', - } ); - lightbox = page.getByRole( 'dialog' ); - closeButton = lightbox.getByRole( 'button', { - name: 'Close', - } ); - } ); - - test( 'should open and focus appropriately using enter key', async ( { - page, - } ) => { - // Open and close lightbox using the close button - await openLightboxButton.focus(); - await page.keyboard.press( 'Enter' ); - await expect( lightbox ).toBeVisible(); - await expect( closeButton ).toBeFocused(); - } ); - - test( 'should close and focus appropriately using enter key on close button', async ( { - page, - } ) => { - // Open and close lightbox using the close button - await openLightboxButton.focus(); - await page.keyboard.press( 'Enter' ); - await expect( lightbox ).toBeVisible(); - await expect( closeButton ).toBeFocused(); - await page.keyboard.press( 'Enter' ); - await expect( lightbox ).toBeHidden(); - await expect( openLightboxButton ).toBeFocused(); - } ); - - test( 'should close and focus appropriately using escape key', async ( { - page, - } ) => { - await openLightboxButton.focus(); - await page.keyboard.press( 'Enter' ); - await expect( lightbox ).toBeVisible(); - await expect( closeButton ).toBeFocused(); - await page.keyboard.press( 'Escape' ); - await expect( lightbox ).toBeHidden(); - await expect( openLightboxButton ).toBeFocused(); - } ); - - // TO DO: Add these tests, which will involve adding a caption - // to uploaded test images - // test( 'should trap focus appropriately when using tab', async ( { - // page, - // } ) => { - - // } ); - - // test( 'should trap focus appropriately using shift+tab', async ( { - // page, - // } ) => { - - // } ); - } ); - } ); - - test( 'lightbox should work as expected when inserting image from URL', async ( { - editor, - page, - } ) => { - await editor.openDocumentSettingsSidebar(); - - const imageBlockFromUrl = editor.canvas.locator( - 'role=document[name="Block: Image"i]' - ); - await expect( imageBlockFromUrl ).toBeVisible(); - - await imageBlockFromUrl - .getByRole( 'button' ) - .filter( { hasText: 'Insert from URL' } ) - .click(); - - const form = page.locator( - '.block-editor-media-placeholder__url-input-form' - ); - - const imgUrl = - 'https://wp20.wordpress.net/wp-content/themes/twentyseventeen-wp20/images/wp20-logo-white.svg'; - - await form.getByLabel( 'URL' ).fill( imgUrl ); - - await form.getByRole( 'button', { name: 'Apply' } ).click(); - - const image = imageBlockFromUrl.locator( 'role=img' ); - await expect( image ).toBeVisible(); - await expect( image ).toHaveAttribute( 'src', imgUrl ); - - await page.getByRole( 'button', { name: 'Advanced' } ).click(); - await page - .getByRole( 'combobox', { name: 'Behaviors' } ) - .selectOption( 'lightbox' ); - - const postId = await editor.publishPost(); - await page.goto( `/?p=${ postId }` ); - - const lightbox = page.locator( '.wp-lightbox-overlay' ); - const responsiveImage = lightbox.locator( '.responsive-image img' ); - const enlargedImage = lightbox.locator( '.enlarged-image img' ); - - await expect( responsiveImage ).toHaveAttribute( - 'src', - new RegExp( imgUrl ) - ); - await expect( enlargedImage ).toHaveAttribute( 'src', '' ); - - await page.getByRole( 'button', { name: 'Enlarge image' } ).click(); - - await expect( responsiveImage ).toHaveAttribute( 'src', imgUrl ); - await expect( enlargedImage ).toHaveAttribute( 'src', imgUrl ); - - await page.getByRole( 'button', { name: 'Close' } ).click(); - - await expect( responsiveImage ).toHaveAttribute( 'src', imgUrl ); - await expect( enlargedImage ).toHaveAttribute( 'src', imgUrl ); } ); } ); diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js index 10f25d6b3609f..7c42ad7989aff 100644 --- a/test/e2e/specs/editor/blocks/list.spec.js +++ b/test/e2e/specs/editor/blocks/list.spec.js @@ -24,7 +24,7 @@ test.describe( 'List (@firefox)', () => { await pageUtils.pressKeys( 'primary+v' ); const copied = `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -53,7 +53,7 @@ test.describe( 'List (@firefox)', () => { await page.keyboard.type( 'Another list item' ); await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>A list item</li> <!-- /wp:list-item --> @@ -78,7 +78,7 @@ test.describe( 'List (@firefox)', () => { await page.keyboard.type( '* ' ); await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>test</li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` @@ -97,7 +97,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>A list item</li> <!-- /wp:list-item --></ol> <!-- /wp:list -->` @@ -264,7 +264,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>I’m a list</li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` @@ -283,7 +283,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>test</li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` @@ -309,7 +309,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -335,7 +335,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -368,7 +368,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one<br>...</li> <!-- /wp:list-item --> @@ -433,7 +433,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:quote --> <blockquote class="wp-block-quote"><!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -457,7 +457,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --></ul> <!-- /wp:list --> @@ -473,7 +473,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -498,7 +498,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -516,7 +516,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --></ul> <!-- /wp:list --> @@ -526,7 +526,7 @@ test.describe( 'List (@firefox)', () => { <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>two</li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` @@ -541,7 +541,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -637,7 +637,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --></ol> <!-- /wp:list --> @@ -647,7 +647,7 @@ test.describe( 'List (@firefox)', () => { <!-- /wp:paragraph --> <!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>two</li> <!-- /wp:list-item --></ol> <!-- /wp:list -->` @@ -665,9 +665,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>two</li> <!-- /wp:list-item --> @@ -691,9 +691,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>one<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li></li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -708,7 +708,7 @@ test.describe( 'List (@firefox)', () => { await editor.clickBlockToolbarButton( 'Ordered' ); await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li></li> <!-- /wp:list-item --></ol> <!-- /wp:list -->` @@ -729,9 +729,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a<!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --></ol> <!-- /wp:list --></li> @@ -754,7 +754,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:quote --> <blockquote class="wp-block-quote"><!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>aaa</li> <!-- /wp:list-item --></ul> <!-- /wp:list --> @@ -775,9 +775,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -789,7 +789,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --> @@ -812,11 +812,11 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>i</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -830,9 +830,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --> @@ -851,9 +851,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -882,11 +882,11 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>b<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>c</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -901,13 +901,13 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>b<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>c</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -927,7 +927,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a<br></li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` @@ -950,7 +950,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --> @@ -981,11 +981,11 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>i</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -1000,9 +1000,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --> @@ -1018,9 +1018,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -1036,9 +1036,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -1051,7 +1051,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --> @@ -1065,7 +1065,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` @@ -1098,13 +1098,13 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>2<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -1128,7 +1128,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --> @@ -1182,9 +1182,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>2</li> <!-- /wp:list-item --> @@ -1208,9 +1208,9 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>2</li> <!-- /wp:list-item --> @@ -1243,7 +1243,7 @@ test.describe( 'List (@firefox)', () => { <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>2</li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` @@ -1267,7 +1267,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --> @@ -1299,7 +1299,7 @@ test.describe( 'List (@firefox)', () => { await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --> @@ -1325,7 +1325,7 @@ test.describe( 'List (@firefox)', () => { // Add empty list block await page.getByPlaceholder( 'Start writing with text or HTML' ) .fill( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li></li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` ); @@ -1335,7 +1335,7 @@ test.describe( 'List (@firefox)', () => { // Verify no WSOD and content is proper. expect( await editor.getEditedPostContent() ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li></li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` ); @@ -1356,7 +1356,7 @@ test.describe( 'List (@firefox)', () => { await page.keyboard.type( '* c' ); await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --> @@ -1366,7 +1366,7 @@ test.describe( 'List (@firefox)', () => { <!-- /wp:list --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>c</li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` ); @@ -1375,7 +1375,7 @@ test.describe( 'List (@firefox)', () => { await page.keyboard.press( 'Backspace' ); await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>a</li> <!-- /wp:list-item --> @@ -1398,7 +1398,7 @@ test.describe( 'List (@firefox)', () => { await page.keyboard.type( '1' ); expect( await editor.getEditedPostContent() ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li></li> <!-- /wp:list-item --></ul> <!-- /wp:list --> @@ -1439,7 +1439,7 @@ test.describe( 'List (@firefox)', () => { await page.getByRole( 'menuitem', { name: 'List' } ).click(); expect( await editor.getEditedPostContent() ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --> diff --git a/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-paste-preformatted-in-list-1-chromium.txt b/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-paste-preformatted-in-list-1-chromium.txt index 002e7a1920028..d46df0dfb55d4 100644 --- a/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-paste-preformatted-in-list-1-chromium.txt +++ b/test/e2e/specs/editor/various/__snapshots__/Copy-cut-paste-should-paste-preformatted-in-list-1-chromium.txt @@ -1,5 +1,5 @@ <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>xy</li> <!-- /wp:list-item --></ul> <!-- /wp:list --> \ No newline at end of file diff --git a/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js b/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js index 0e8c5c8e7bf53..1962e2bc4202a 100644 --- a/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js +++ b/test/e2e/specs/editor/various/block-editor-keyboard-shortcuts.spec.js @@ -217,4 +217,68 @@ test.describe( 'Block editor keyboard shortcuts', () => { ] ); } ); } ); + + test.describe( 'create a group block from the selected blocks', () => { + test( 'should propagate properly if multiple blocks are selected.', async ( { + editor, + page, + pageUtils, + } ) => { + await addTestParagraphBlocks( { editor, page } ); + + // Multiselect via keyboard. + await pageUtils.pressKeys( 'primary+a', { times: 2 } ); + + await pageUtils.pressKeys( 'primary+g' ); // Keyboard shortcut for Insert before. + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: '1st' }, + }, + { + name: 'core/paragraph', + attributes: { content: '2nd' }, + }, + { + name: 'core/paragraph', + attributes: { content: '3rd' }, + }, + ], + }, + ] ); + } ); + + test( 'should prevent if a single block is selected.', async ( { + editor, + page, + pageUtils, + } ) => { + await addTestParagraphBlocks( { editor, page } ); + const firstParagraphBlock = editor.canvas + .getByRole( 'document', { + name: 'Block: Paragraph', + } ) + .first(); + await editor.selectBlocks( firstParagraphBlock ); + await pageUtils.pressKeys( 'primary+g' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: '1st' }, + }, + { + name: 'core/paragraph', + attributes: { content: '2nd' }, + }, + { + name: 'core/paragraph', + attributes: { content: '3rd' }, + }, + ] ); + } ); + } ); } ); diff --git a/test/e2e/specs/editor/various/block-renaming.spec.js b/test/e2e/specs/editor/various/block-renaming.spec.js index c7304023b970c..6dc1e9a3a49f4 100644 --- a/test/e2e/specs/editor/various/block-renaming.spec.js +++ b/test/e2e/specs/editor/various/block-renaming.spec.js @@ -36,26 +36,10 @@ test.describe( 'Block Renaming', () => { page, pageUtils, } ) => { - // Turn on block list view by default. - await editor.setPreferences( 'core', { - showListViewByDefault: true, - } ); - - const listView = page.getByRole( 'treegrid', { - name: 'Block navigation structure', - } ); - - await editor.insertBlock( { - name: 'core/group', - attributes: { content: 'First Paragraph' }, - } ); + await editor.insertBlock( { name: 'core/group' } ); // Select via keyboard. await pageUtils.pressKeys( 'primary+a' ); - - // Convert to a Group block which supports renaming. - await editor.clickBlockOptionsMenuItem( 'Group' ); - await editor.clickBlockOptionsMenuItem( 'Rename' ); const renameMenuItem = page.getByRole( 'menuitem', { @@ -107,11 +91,18 @@ test.describe( 'Block Renaming', () => { 'false' ); - // Check custom name reflected in List View. - listView.getByRole( 'link', { - name: 'My new name', + await pageUtils.pressKeys( 'access+o' ); + const listView = page.getByRole( 'treegrid', { + name: 'Block navigation structure', } ); + await expect( + listView.getByRole( 'link', { + name: 'My new name', + } ), + 'should reflect custom name in List View' + ).toBeVisible(); + await expect.poll( editor.getBlocks ).toMatchObject( [ { name: 'core/group', @@ -123,7 +114,8 @@ test.describe( 'Block Renaming', () => { }, ] ); - // Re-trigger the rename dialog. + // Re-trigger the rename dialog from the List View. + await listView.getByRole( 'button', { name: 'Options' } ).click(); await renameMenuItem.click(); // Expect modal input to contain the custom name. @@ -142,10 +134,12 @@ test.describe( 'Block Renaming', () => { await saveButton.click(); - // Check the original block name to reflected in List View. - listView.getByRole( 'link', { - name: 'Group', - } ); + await expect( + listView.getByRole( 'link', { + name: 'Group', + } ), + 'should reflect original name in List View' + ).toBeVisible(); // Expect block to have no custom name (i.e. it should be reset to the original block name). await expect.poll( editor.getBlocks ).toMatchObject( [ diff --git a/test/e2e/specs/editor/various/change-detection.spec.js b/test/e2e/specs/editor/various/change-detection.spec.js index 4f539a92269e6..36496faecc3ad 100644 --- a/test/e2e/specs/editor/various/change-detection.spec.js +++ b/test/e2e/specs/editor/various/change-detection.spec.js @@ -411,19 +411,17 @@ test.describe( 'Change detection', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { name: 'Move to trash' } ) + .getByRole( 'button', { name: 'Actions' } ) + .click(); + await page + .getByRole( 'menu' ) + .getByRole( 'menuitem', { name: 'Move to Trash' } ) .click(); await page .getByRole( 'dialog' ) - .getByRole( 'button', { name: 'Move to trash' } ) + .getByRole( 'button', { name: 'Delete' } ) .click(); - await expect( - page - .getByRole( 'region', { name: 'Editor top bar' } ) - .getByRole( 'button', { name: 'saved' } ) - ).toBeDisabled(); - await expect( page ).toHaveURL( '/wp-admin/edit.php?post_type=post' ); } ); diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js index c277b056a323c..b3ff3ae1b72e5 100644 --- a/test/e2e/specs/editor/various/inserting-blocks.spec.js +++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js @@ -628,7 +628,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { .click(); await page.getByRole( 'option', { name: 'More', exact: true } ).click(); - // Moving focus to the More block should close the inserter. + // Moving focus to the More block should not close the inserter. await editor.canvas .getByRole( 'textbox', { name: 'Read more' } ) .fill( 'More' ); @@ -636,7 +636,7 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { page.getByRole( 'region', { name: 'Block Library', } ) - ).toBeHidden(); + ).toBeVisible(); } ); test( 'shows block preview when hovering over block in inserter', async ( { diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js index 8c14711084389..143fea43c09ee 100644 --- a/test/e2e/specs/editor/various/list-view.spec.js +++ b/test/e2e/specs/editor/various/list-view.spec.js @@ -986,6 +986,45 @@ test.describe( 'List View', () => { ] ); } ); + test( 'should create a group block from the selected multiple blocks', async ( { + editor, + pageUtils, + listViewUtils, + } ) => { + // Insert some blocks of different types. + await editor.insertBlock( { name: 'core/paragraph' } ); + await editor.insertBlock( { name: 'core/heading' } ); + await editor.insertBlock( { name: 'core/file' } ); + + await listViewUtils.openListView(); + + // Group Heading and File blocks. + await pageUtils.pressKeys( 'shift+ArrowUp' ); + await pageUtils.pressKeys( 'primary+g' ); + await expect + .poll( listViewUtils.getBlocksWithA11yAttributes ) + .toMatchObject( [ + { name: 'core/paragraph', selected: false, focused: false }, + { + name: 'core/group', + selected: true, + focused: true, + innerBlocks: [ + { + name: 'core/heading', + selected: false, + focused: false, + }, + { + name: 'core/file', + selected: false, + focused: false, + }, + ], + }, + ] ); + } ); + test( 'block settings dropdown menu', async ( { editor, page, diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js index 91aafa43690fd..2dee26255c103 100644 --- a/test/e2e/specs/editor/various/writing-flow.spec.js +++ b/test/e2e/specs/editor/various/writing-flow.spec.js @@ -356,7 +356,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { await page.keyboard.type( 'a' ); await page.keyboard.press( 'Backspace' ); await expect.poll( editor.getEditedPostContent ).toBe( `<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li></li> <!-- /wp:list-item --></ul> <!-- /wp:list -->` ); diff --git a/test/e2e/specs/interactivity/directive-bind.spec.ts b/test/e2e/specs/interactivity/directive-bind.spec.ts index 525fceab5ca6a..11902018e0753 100644 --- a/test/e2e/specs/interactivity/directive-bind.spec.ts +++ b/test/e2e/specs/interactivity/directive-bind.spec.ts @@ -210,6 +210,12 @@ test.describe( 'data-wp-bind', () => { const el = container.getByTestId( testid ); const toggle = container.getByTestId( 'toggle value' ); + // Ensure hydration has happened. + const checkbox = page.getByTestId( + 'add missing checked at hydration' + ); + await expect( checkbox ).toBeChecked(); + const hydratedAttr = await el.getAttribute( name ); const hydratedProp = await el.evaluate( ( node, propName ) => ( node as any )[ propName ], @@ -236,7 +242,13 @@ test.describe( 'data-wp-bind', () => { return; } - await toggle.click( { clickCount: 2, delay: 50 } ); + await toggle.click( { clickCount: 2 } ); + + // Ensure values have been updated after toggling. + await expect( toggle ).toHaveAttribute( + 'data-toggle-count', + '2' + ); // Values should be the same as before. const renderedAttr = await el.getAttribute( name ); diff --git a/test/e2e/specs/interactivity/directive-on-document.spec.ts b/test/e2e/specs/interactivity/directive-on-document.spec.ts index 2b4f36f51efcc..62d122ee2359e 100644 --- a/test/e2e/specs/interactivity/directive-on-document.spec.ts +++ b/test/e2e/specs/interactivity/directive-on-document.spec.ts @@ -65,4 +65,25 @@ test.describe( 'data-wp-on-document', () => { await page.keyboard.press( 'ArrowDown' ); await expect( counter ).toHaveText( '2' ); } ); + test( 'should work with multiple event handlers on the same event type', async ( { + page, + } ) => { + const keydownHandler = page.getByTestId( 'keydownHandler' ); + const keydownSecondHandler = page.getByTestId( 'keydownSecondHandler' ); + + // Initial value. + await expect( keydownHandler ).toHaveText( 'no' ); + await expect( keydownSecondHandler ).toHaveText( 'no' ); + + // Make sure the event listener is attached. + await page + .getByTestId( 'isEventAttached' ) + .filter( { hasText: 'yes' } ) + .waitFor(); + + // This keyboard press should increase the counter. + await page.keyboard.press( 'ArrowDown' ); + await expect( keydownHandler ).toHaveText( 'yes' ); + await expect( keydownSecondHandler ).toHaveText( 'yes' ); + } ); } ); diff --git a/test/e2e/specs/interactivity/directive-on-window.spec.ts b/test/e2e/specs/interactivity/directive-on-window.spec.ts index 3afd1a9942fa8..ed090839c49ad 100644 --- a/test/e2e/specs/interactivity/directive-on-window.spec.ts +++ b/test/e2e/specs/interactivity/directive-on-window.spec.ts @@ -65,4 +65,25 @@ test.describe( 'data-wp-on-window', () => { await page.setViewportSize( { width: 200, height: 600 } ); await expect( counter ).toHaveText( '2' ); } ); + test( 'should work with multiple event handlers on the same event type', async ( { + page, + } ) => { + const resizeHandler = page.getByTestId( 'resizeHandler' ); + const resizeSecondHandler = page.getByTestId( 'resizeSecondHandler' ); + + // Initial value. + await expect( resizeHandler ).toHaveText( 'no' ); + await expect( resizeSecondHandler ).toHaveText( 'no' ); + + // Make sure the event listener is attached. + await page + .getByTestId( 'isEventAttached' ) + .filter( { hasText: 'yes' } ) + .waitFor(); + + // This keyboard press should increase the counter. + await page.setViewportSize( { width: 600, height: 600 } ); + await expect( resizeHandler ).toHaveText( 'yes' ); + await expect( resizeSecondHandler ).toHaveText( 'yes' ); + } ); } ); diff --git a/test/e2e/specs/site-editor/block-removal.spec.js b/test/e2e/specs/site-editor/block-removal.spec.js index 2b149bcc69bf3..7d656fbd2774f 100644 --- a/test/e2e/specs/site-editor/block-removal.spec.js +++ b/test/e2e/specs/site-editor/block-removal.spec.js @@ -25,7 +25,9 @@ test.describe( 'Site editor block removal prompt', () => { } ) => { // Open and focus List View const topBar = page.getByRole( 'region', { name: 'Editor top bar' } ); - await topBar.getByRole( 'button', { name: 'List View' } ).click(); + await topBar + .getByRole( 'button', { name: 'Document Overview' } ) + .click(); // Select and try to remove Query Loop block const listView = page.getByRole( 'region', { name: 'List View' } ); @@ -45,7 +47,9 @@ test.describe( 'Site editor block removal prompt', () => { } ) => { // Open and focus List View const topBar = page.getByRole( 'region', { name: 'Editor top bar' } ); - await topBar.getByRole( 'button', { name: 'List View' } ).click(); + await topBar + .getByRole( 'button', { name: 'Document Overview' } ) + .click(); // Select and open child blocks of Query Loop block const listView = page.getByRole( 'region', { name: 'List View' } ); @@ -70,7 +74,9 @@ test.describe( 'Site editor block removal prompt', () => { } ) => { // Open and focus List View const topBar = page.getByRole( 'region', { name: 'Editor top bar' } ); - await topBar.getByRole( 'button', { name: 'List View' } ).click(); + await topBar + .getByRole( 'button', { name: 'Document Overview' } ) + .click(); // Select Query Loop list item const listView = page.getByRole( 'region', { name: 'List View' } ); diff --git a/test/e2e/specs/site-editor/list-view.spec.js b/test/e2e/specs/site-editor/list-view.spec.js index 9b7a1c17a9ce1..9bccc7c56446a 100644 --- a/test/e2e/specs/site-editor/list-view.spec.js +++ b/test/e2e/specs/site-editor/list-view.spec.js @@ -106,7 +106,7 @@ test.describe( 'Site Editor List View', () => { // Focus should now be on the list view toggle button. await expect( - page.getByRole( 'button', { name: 'List View' } ) + page.getByRole( 'button', { name: 'Document Overview' } ) ).toBeFocused(); // Open List View. @@ -129,7 +129,7 @@ test.describe( 'Site Editor List View', () => { await pageUtils.pressKeys( 'access+o' ); await expect( listView ).toBeHidden(); await expect( - page.getByRole( 'button', { name: 'List View' } ) + page.getByRole( 'button', { name: 'Document Overview' } ) ).toBeFocused(); } ); } ); diff --git a/test/e2e/specs/site-editor/style-book.spec.js b/test/e2e/specs/site-editor/style-book.spec.js index 4a5864755614b..087013607be2f 100644 --- a/test/e2e/specs/site-editor/style-book.spec.js +++ b/test/e2e/specs/site-editor/style-book.spec.js @@ -30,18 +30,12 @@ test.describe( 'Style Book', () => { test( 'should disable toolbar buttons when open', async ( { page } ) => { await expect( page.locator( 'role=button[name="Toggle block inserter"i]' ) - ).toBeHidden(); + ).toBeDisabled(); await expect( page.locator( 'role=button[name="Tools"i]' ) - ).toBeHidden(); - await expect( - page.locator( 'role=button[name="Undo"i]' ) - ).toBeHidden(); - await expect( - page.locator( 'role=button[name="Redo"i]' ) - ).toBeHidden(); + ).toBeDisabled(); await expect( - page.locator( 'role=button[name="View"i]' ) + page.locator( 'role=button[name="Document Overview"i]' ) ).toBeDisabled(); } ); diff --git a/test/e2e/specs/site-editor/template-revert.spec.js b/test/e2e/specs/site-editor/template-revert.spec.js index 2af2a90eae119..5c19ed5a39e00 100644 --- a/test/e2e/specs/site-editor/template-revert.spec.js +++ b/test/e2e/specs/site-editor/template-revert.spec.js @@ -56,7 +56,7 @@ test.describe( 'Template Revert', () => { page.locator( 'role=region[name="Editor settings"i] >> role=button[name="Actions"i]' ) - ).toBeHidden(); + ).toBeDisabled(); } ); test( 'should show the original content after revert', async ( { @@ -179,6 +179,10 @@ test.describe( 'Template Revert', () => { await editor.saveSiteEditorEntities( { isOnlyCurrentEntityDirty: true, } ); + await page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .getByText( /(updated|published)\./ ) + .click(); const contentBefore = await templateRevertUtils.getCurrentSiteEditorContent(); @@ -190,7 +194,6 @@ test.describe( 'Template Revert', () => { await editor.saveSiteEditorEntities( { isOnlyCurrentEntityDirty: true, } ); - await admin.visitSiteEditor(); const contentAfter = diff --git a/test/e2e/specs/site-editor/undo.spec.js b/test/e2e/specs/site-editor/undo.spec.js new file mode 100644 index 0000000000000..17c4e4ada6e61 --- /dev/null +++ b/test/e2e/specs/site-editor/undo.spec.js @@ -0,0 +1,42 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'undo', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'does not empty header', async ( { admin, page, editor } ) => { + await admin.visitSiteEditor( { canvas: 'edit' } ); + + // Check if there's a valid child block with a type (not appender). + await expect( + editor.canvas.locator( + '[data-type="core/template-part"] [data-type]' + ) + ).not.toHaveCount( 0 ); + + // insert a block + await editor.insertBlock( { name: 'core/paragraph' } ); + + // undo + await page + .getByRole( 'button', { + name: 'Undo', + } ) + .click(); + + // Check if there's a valid child block with a type (not appender). + await expect( + editor.canvas.locator( + '[data-type="core/template-part"] [data-type]' + ) + ).not.toHaveCount( 0 ); + } ); +} ); diff --git a/test/e2e/specs/site-editor/zoom-out.spec.js b/test/e2e/specs/site-editor/zoom-out.spec.js index 0a4319440f4ac..63ad58b2e18a5 100644 --- a/test/e2e/specs/site-editor/zoom-out.spec.js +++ b/test/e2e/specs/site-editor/zoom-out.spec.js @@ -8,57 +8,31 @@ test.describe( 'Zoom Out', () => { await requestUtils.activateTheme( 'emptytheme' ); } ); - test.beforeEach( async ( { admin, page, editor } ) => { - await admin.visitAdminPage( 'admin.php', 'page=gutenberg-experiments' ); - - const zoomedOutCheckbox = page.getByLabel( - 'Test a new zoomed out view on' - ); - - await zoomedOutCheckbox.setChecked( true ); - await expect( zoomedOutCheckbox ).toBeChecked(); - await page.getByRole( 'button', { name: 'Save Changes' } ).click(); - - // Select a template part with a few blocks. - await admin.visitSiteEditor( { - postId: 'emptytheme//header', - postType: 'wp_template_part', - } ); + test.beforeEach( async ( { admin, editor } ) => { + await admin.visitSiteEditor(); await editor.canvas.locator( 'body' ).click(); } ); - test.afterEach( async ( { admin, page } ) => { - await admin.visitAdminPage( 'admin.php', 'page=gutenberg-experiments' ); - const zoomedOutCheckbox = page.getByLabel( - 'Test a new zoomed out view on' - ); - await zoomedOutCheckbox.setChecked( false ); - await expect( zoomedOutCheckbox ).not.toBeChecked(); - await page.getByRole( 'button', { name: 'Save Changes' } ).click(); - } ); - test.afterAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'twentytwentyone' ); } ); - test( 'Zoom-out button should not steal focus when a block is focused', async ( { + test( 'Clicking on inserter while on zoom-out should open the patterns tab on the inserter', async ( { page, - editor, } ) => { - const zoomOutButton = page.getByRole( 'button', { - name: 'Zoom-out View', - exact: true, - } ); - - // Select a block for this test to surface the potential focus-stealing behavior - await editor.canvas.getByLabel( 'Site title text' ).click(); - - await zoomOutButton.click(); - - await expect( zoomOutButton ).toBeFocused(); - - await page.keyboard.press( 'Enter' ); - - await expect( zoomOutButton ).toBeFocused(); + // Trigger zoom out on Global Styles because there the inserter is not open. + await page.getByRole( 'button', { name: 'Styles' } ).click(); + await page.getByRole( 'button', { name: 'Browse styles' } ).click(); + + await expect( page.getByLabel( 'Add pattern' ) ).toHaveCount( 3 ); + await page.getByLabel( 'Add pattern' ).first().click(); + await expect( page.getByLabel( 'Add pattern' ) ).toHaveCount( 2 ); + + await expect( + page + .locator( '#tabs-2-allPatterns-view div' ) + .filter( { hasText: 'All' } ) + .nth( 1 ) + ).toBeVisible(); } ); } ); diff --git a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap index b810a20b4d7c5..589eef78f34e2 100644 --- a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap +++ b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap @@ -135,7 +135,7 @@ exports[`rawHandler should convert HTML post to blocks with minimal content chan <!-- /wp:html --> <!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>Item</li> <!-- /wp:list-item --></ol> <!-- /wp:list --> @@ -177,9 +177,9 @@ exports[`rawHandler should convert a caption shortcode with link 1`] = ` exports[`rawHandler should convert a list with attributes 1`] = ` "<!-- wp:list {"ordered":true,"type":"lower-roman","start":2,"reversed":true} --> -<ol reversed start="2" style="list-style-type:lower-roman"><!-- wp:list-item --> +<ol reversed start="2" style="list-style-type:lower-roman" class="wp-block-list"><!-- wp:list-item --> <li>1<!-- wp:list {"ordered":true,"type":"lower-roman","start":2,"reversed":true} --> -<ol reversed start="2" style="list-style-type:lower-roman"><!-- wp:list-item --> +<ol reversed start="2" style="list-style-type:lower-roman" class="wp-block-list"><!-- wp:list-item --> <li>1</li> <!-- /wp:list-item --></ol> <!-- /wp:list --></li> diff --git a/test/integration/blocks-raw-handling.test.js b/test/integration/blocks-raw-handling.test.js index 5fb7123f9b820..81d3b7053468b 100644 --- a/test/integration/blocks-raw-handling.test.js +++ b/test/integration/blocks-raw-handling.test.js @@ -177,7 +177,7 @@ describe( 'Blocks raw handling', () => { .join( '' ); expect( filtered ).toMatchInlineSnapshot( ` - "<ul><!-- wp:list-item --> + "<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -202,7 +202,7 @@ describe( 'Blocks raw handling', () => { .join( '' ); expect( filtered ).toMatchInlineSnapshot( ` - "<ul><!-- wp:list-item --> + "<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -318,7 +318,7 @@ describe( 'Blocks raw handling', () => { .join( '' ); expect( filtered ).toMatchInlineSnapshot( ` - "<ul><!-- wp:list-item --> + "<ul class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/blocks/core__list__deprecated-v0.serialized.html b/test/integration/fixtures/blocks/core__list__deprecated-v0.serialized.html index fa50fb5db17e7..ad0919beb9bde 100644 --- a/test/integration/fixtures/blocks/core__list__deprecated-v0.serialized.html +++ b/test/integration/fixtures/blocks/core__list__deprecated-v0.serialized.html @@ -1,5 +1,5 @@ <!-- wp:list {"fontFamily":"cambria-georgia"} --> -<ul class="has-cambria-georgia-font-family"><!-- wp:list-item --> +<ul class="wp-block-list has-cambria-georgia-font-family"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/blocks/core__list__deprecated-v1-nested.serialized.html b/test/integration/fixtures/blocks/core__list__deprecated-v1-nested.serialized.html index 61e33d3dc0e15..39a3efa2eabed 100644 --- a/test/integration/fixtures/blocks/core__list__deprecated-v1-nested.serialized.html +++ b/test/integration/fixtures/blocks/core__list__deprecated-v1-nested.serialized.html @@ -1,9 +1,9 @@ <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li><strong>Item</strong><!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li><strong>Item</strong><!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li><a rel="noreferrer noopener" href="#" target="_blank">Item</a></li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -11,7 +11,7 @@ <!-- wp:list-item --> <li><strong>Item</strong><!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li><a rel="noreferrer noopener" href="#" target="_blank">Item</a></li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -19,7 +19,7 @@ <!-- wp:list-item --> <li><strong>Item</strong><!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li><a rel="noreferrer noopener" href="#" target="_blank">Item</a></li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> diff --git a/test/integration/fixtures/blocks/core__list__deprecated-v1.serialized.html b/test/integration/fixtures/blocks/core__list__deprecated-v1.serialized.html index c09708d51db0b..4d74394771fcb 100644 --- a/test/integration/fixtures/blocks/core__list__deprecated-v1.serialized.html +++ b/test/integration/fixtures/blocks/core__list__deprecated-v1.serialized.html @@ -1,5 +1,5 @@ <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Text &amp; Headings</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/blocks/core__list__deprecated-v2.serialized.html b/test/integration/fixtures/blocks/core__list__deprecated-v2.serialized.html index 44c97ca29209c..6fdf3ad84feeb 100644 --- a/test/integration/fixtures/blocks/core__list__deprecated-v2.serialized.html +++ b/test/integration/fixtures/blocks/core__list__deprecated-v2.serialized.html @@ -1,5 +1,5 @@ <!-- wp:list {"ordered":true,"type":"upper-alpha"} --> -<ol style="list-style-type:upper-alpha"><!-- wp:list-item --> +<ol style="list-style-type:upper-alpha" class="wp-block-list"><!-- wp:list-item --> <li>Item 1</li> <!-- /wp:list-item --></ol> <!-- /wp:list --> diff --git a/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.html b/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.html new file mode 100644 index 0000000000000..aef6bf669f2c9 --- /dev/null +++ b/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.html @@ -0,0 +1,3 @@ +<!-- wp:core/list --> +<ul><li>Text & Headings</li><li>Images & Videos</li><li>Galleries</li><li>Embeds, like YouTube, Tweets, or other WordPress posts.</li><li>Layout blocks, like Buttons, Hero Images, Separators, etc.</li><li>And <em>Lists</em> like this one of course :)</li></ul> +<!-- /wp:core/list --> diff --git a/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.json b/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.json new file mode 100644 index 0000000000000..80cd87e4eb69e --- /dev/null +++ b/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.json @@ -0,0 +1,60 @@ +[ + { + "name": "core/list", + "isValid": true, + "attributes": { + "ordered": false, + "values": "" + }, + "innerBlocks": [ + { + "name": "core/list-item", + "isValid": true, + "attributes": { + "content": "Text &amp; Headings" + }, + "innerBlocks": [] + }, + { + "name": "core/list-item", + "isValid": true, + "attributes": { + "content": "Images &amp; Videos" + }, + "innerBlocks": [] + }, + { + "name": "core/list-item", + "isValid": true, + "attributes": { + "content": "Galleries" + }, + "innerBlocks": [] + }, + { + "name": "core/list-item", + "isValid": true, + "attributes": { + "content": "Embeds, like YouTube, Tweets, or other WordPress posts." + }, + "innerBlocks": [] + }, + { + "name": "core/list-item", + "isValid": true, + "attributes": { + "content": "Layout blocks, like Buttons, Hero Images, Separators, etc." + }, + "innerBlocks": [] + }, + { + "name": "core/list-item", + "isValid": true, + "attributes": { + "content": "And <em>Lists</em> like this one of course :)" + }, + "innerBlocks": [] + } + ] + } +] diff --git a/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.parsed.json b/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.parsed.json new file mode 100644 index 0000000000000..427f4c3a975cf --- /dev/null +++ b/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.parsed.json @@ -0,0 +1,11 @@ +[ + { + "blockName": "core/list", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n<ul><li>Text & Headings</li><li>Images & Videos</li><li>Galleries</li><li>Embeds, like YouTube, Tweets, or other WordPress posts.</li><li>Layout blocks, like Buttons, Hero Images, Separators, etc.</li><li>And <em>Lists</em> like this one of course :)</li></ul>\n", + "innerContent": [ + "\n<ul><li>Text & Headings</li><li>Images & Videos</li><li>Galleries</li><li>Embeds, like YouTube, Tweets, or other WordPress posts.</li><li>Layout blocks, like Buttons, Hero Images, Separators, etc.</li><li>And <em>Lists</em> like this one of course :)</li></ul>\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.serialized.html b/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.serialized.html new file mode 100644 index 0000000000000..4d74394771fcb --- /dev/null +++ b/test/integration/fixtures/blocks/core__list__deprecated-v3-css-class.serialized.html @@ -0,0 +1,25 @@ +<!-- wp:list --> +<ul class="wp-block-list"><!-- wp:list-item --> +<li>Text &amp; Headings</li> +<!-- /wp:list-item --> + +<!-- wp:list-item --> +<li>Images &amp; Videos</li> +<!-- /wp:list-item --> + +<!-- wp:list-item --> +<li>Galleries</li> +<!-- /wp:list-item --> + +<!-- wp:list-item --> +<li>Embeds, like YouTube, Tweets, or other WordPress posts.</li> +<!-- /wp:list-item --> + +<!-- wp:list-item --> +<li>Layout blocks, like Buttons, Hero Images, Separators, etc.</li> +<!-- /wp:list-item --> + +<!-- wp:list-item --> +<li>And <em>Lists</em> like this one of course :)</li> +<!-- /wp:list-item --></ul> +<!-- /wp:list --> diff --git a/test/integration/fixtures/blocks/core__list__ul.html b/test/integration/fixtures/blocks/core__list__ul.html index c09708d51db0b..4d74394771fcb 100644 --- a/test/integration/fixtures/blocks/core__list__ul.html +++ b/test/integration/fixtures/blocks/core__list__ul.html @@ -1,5 +1,5 @@ <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Text &amp; Headings</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/blocks/core__list__ul.parsed.json b/test/integration/fixtures/blocks/core__list__ul.parsed.json index 2853bc5558ebe..4639b007c777b 100644 --- a/test/integration/fixtures/blocks/core__list__ul.parsed.json +++ b/test/integration/fixtures/blocks/core__list__ul.parsed.json @@ -52,9 +52,9 @@ ] } ], - "innerHTML": "\n<ul>\n\n\n\n\n\n\n\n\n\n</ul>\n", + "innerHTML": "\n<ul class=\"wp-block-list\">\n\n\n\n\n\n\n\n\n\n</ul>\n", "innerContent": [ - "\n<ul>", + "\n<ul class=\"wp-block-list\">", null, "\n\n", null, diff --git a/test/integration/fixtures/blocks/core__list__ul.serialized.html b/test/integration/fixtures/blocks/core__list__ul.serialized.html index c09708d51db0b..4d74394771fcb 100644 --- a/test/integration/fixtures/blocks/core__list__ul.serialized.html +++ b/test/integration/fixtures/blocks/core__list__ul.serialized.html @@ -1,5 +1,5 @@ <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Text &amp; Headings</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/documents/apple-out.html b/test/integration/fixtures/documents/apple-out.html index afada0ee3136e..d6112309af9f0 100644 --- a/test/integration/fixtures/documents/apple-out.html +++ b/test/integration/fixtures/documents/apple-out.html @@ -11,13 +11,13 @@ <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>A</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>Bulleted<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Indented</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -29,7 +29,7 @@ <!-- /wp:list --> <!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/documents/evernote-out.html b/test/integration/fixtures/documents/evernote-out.html index a5ff509c7a173..2a7212f2e29b3 100644 --- a/test/integration/fixtures/documents/evernote-out.html +++ b/test/integration/fixtures/documents/evernote-out.html @@ -7,13 +7,13 @@ <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>An</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>Unordered<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Indented</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -25,13 +25,13 @@ <!-- /wp:list --> <!-- wp:list {"ordered":true,"start":1} --> -<ol start="1"><!-- wp:list-item --> +<ol start="1" class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>Two<!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>Indented</li> <!-- /wp:list-item --></ol> <!-- /wp:list --></li> diff --git a/test/integration/fixtures/documents/google-docs-list-only-out.html b/test/integration/fixtures/documents/google-docs-list-only-out.html index 642dc168bb5bb..ec14d44d2b922 100644 --- a/test/integration/fixtures/documents/google-docs-list-only-out.html +++ b/test/integration/fixtures/documents/google-docs-list-only-out.html @@ -1,7 +1,7 @@ <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>My first list item<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>A sub list item</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/documents/google-docs-out.html b/test/integration/fixtures/documents/google-docs-out.html index f2ec15cf4f92a..3ee7d25875abc 100644 --- a/test/integration/fixtures/documents/google-docs-out.html +++ b/test/integration/fixtures/documents/google-docs-out.html @@ -11,13 +11,13 @@ <h2 class="wp-block-heading">This is a <em>heading</em></h2> <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>A</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>Bulleted<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Indented</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -29,7 +29,7 @@ <h2 class="wp-block-heading">This is a <em>heading</em></h2> <!-- /wp:list --> <!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/documents/google-docs-with-comments-out.html b/test/integration/fixtures/documents/google-docs-with-comments-out.html index f2ec15cf4f92a..3ee7d25875abc 100644 --- a/test/integration/fixtures/documents/google-docs-with-comments-out.html +++ b/test/integration/fixtures/documents/google-docs-with-comments-out.html @@ -11,13 +11,13 @@ <h2 class="wp-block-heading">This is a <em>heading</em></h2> <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>A</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>Bulleted<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Indented</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -29,7 +29,7 @@ <h2 class="wp-block-heading">This is a <em>heading</em></h2> <!-- /wp:list --> <!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/documents/markdown-out.html b/test/integration/fixtures/documents/markdown-out.html index ed57e4e05e717..9ed72f6d3ef6d 100644 --- a/test/integration/fixtures/documents/markdown-out.html +++ b/test/integration/fixtures/documents/markdown-out.html @@ -15,13 +15,13 @@ <h2 class="wp-block-heading">Lists</h2> <!-- /wp:heading --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>A</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>Bulleted<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Indented</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -33,7 +33,7 @@ <h2 class="wp-block-heading">Lists</h2> <!-- /wp:list --> <!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/documents/ms-word-list-out.html b/test/integration/fixtures/documents/ms-word-list-out.html index f57946f64bc98..2e55413b159a6 100644 --- a/test/integration/fixtures/documents/ms-word-list-out.html +++ b/test/integration/fixtures/documents/ms-word-list-out.html @@ -7,7 +7,7 @@ <h3 class="wp-block-heading"><a>This is a headline?</a></h3> <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/documents/ms-word-online-out.html b/test/integration/fixtures/documents/ms-word-online-out.html index 2d8314bbf0fd6..13698a929d9b1 100644 --- a/test/integration/fixtures/documents/ms-word-online-out.html +++ b/test/integration/fixtures/documents/ms-word-online-out.html @@ -7,7 +7,7 @@ <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>A </li> <!-- /wp:list-item --> @@ -25,7 +25,7 @@ <!-- /wp:list --> <!-- wp:list {"ordered":true,"start":1} --> -<ol start="1"><!-- wp:list-item --> +<ol start="1" class="wp-block-list"><!-- wp:list-item --> <li>One </li> <!-- /wp:list-item --> diff --git a/test/integration/fixtures/documents/ms-word-out.html b/test/integration/fixtures/documents/ms-word-out.html index 89eaa6f3bcd16..81b82b5f9502f 100644 --- a/test/integration/fixtures/documents/ms-word-out.html +++ b/test/integration/fixtures/documents/ms-word-out.html @@ -19,13 +19,13 @@ <h2 class="wp-block-heading">This is a heading level 2</h2> <!-- /wp:paragraph --> <!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>A</li> <!-- /wp:list-item --> <!-- wp:list-item --> <li>Bulleted<!-- wp:list --> -<ul><!-- wp:list-item --> +<ul class="wp-block-list"><!-- wp:list-item --> <li>Indented</li> <!-- /wp:list-item --></ul> <!-- /wp:list --></li> @@ -37,7 +37,7 @@ <h2 class="wp-block-heading">This is a heading level 2</h2> <!-- /wp:list --> <!-- wp:list {"ordered":true} --> -<ol><!-- wp:list-item --> +<ol class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> diff --git a/test/native/integration-test-helpers/dismiss-modal.js b/test/native/integration-test-helpers/dismiss-modal.js index 2a687401a586a..c782fd8be185b 100644 --- a/test/native/integration-test-helpers/dismiss-modal.js +++ b/test/native/integration-test-helpers/dismiss-modal.js @@ -6,7 +6,7 @@ import { fireEvent } from '@testing-library/react-native'; /** * Dismisses a modal. * - * @param {HTMLElement} modalInstance Modal test instance. + * @param {import('react-test-renderer').ReactTestInstance} modalInstance Modal test instance. */ export const dismissModal = async ( modalInstance ) => fireEvent( modalInstance, 'backdropPress' ); diff --git a/test/native/integration-test-helpers/get-block-transform-options.js b/test/native/integration-test-helpers/get-block-transform-options.js index f7b008199af41..02ca81a1d5ccc 100644 --- a/test/native/integration-test-helpers/get-block-transform-options.js +++ b/test/native/integration-test-helpers/get-block-transform-options.js @@ -14,7 +14,7 @@ import { openBlockActionsMenu } from './open-block-actions-menu'; * * @param {import('@testing-library/react-native').RenderAPI} screen A Testing Library screen. * @param {string} blockName Name of the block. - * @return {[HTMLElement]} Block transform options. + * @return {[import('react-test-renderer').ReactTestInstance]} Block transform options. */ export const getBlockTransformOptions = async ( screen, blockName ) => { const { getByTestId, getByText } = screen; diff --git a/test/native/integration-test-helpers/get-block.js b/test/native/integration-test-helpers/get-block.js index 0fc36593fe8e1..0770a736affbb 100644 --- a/test/native/integration-test-helpers/get-block.js +++ b/test/native/integration-test-helpers/get-block.js @@ -5,7 +5,7 @@ * @param {string} blockName Name of the block. * @param {Object} options Configuration options for getting the block. * @param {number} [options.rowIndex] Row index of the block. - * @return {HTMLElement} Block instance. + * @return {import('react-test-renderer').ReactTestInstance} Block instance. */ export const getBlock = ( screen, blockName, { rowIndex = 1 } = {} ) => { return screen.getAllByLabelText( diff --git a/test/native/integration-test-helpers/get-inner-block.js b/test/native/integration-test-helpers/get-inner-block.js index 71a0d75da6bee..1bb1e6938c280 100644 --- a/test/native/integration-test-helpers/get-inner-block.js +++ b/test/native/integration-test-helpers/get-inner-block.js @@ -6,11 +6,11 @@ import { within } from '@testing-library/react-native'; /** * Gets an inner block from another block. * - * @param {HTMLElement} parentBlock Parent block from where to get the block. - * @param {string} blockName Name of the block. - * @param {Object} options Configuration options for getting the block. - * @param {number} [options.rowIndex] Row index of the block. - * @return {HTMLElement} Block instance. + * @param {import('react-test-renderer').ReactTestInstance} parentBlock Parent block from where to get the block. + * @param {string} blockName Name of the block. + * @param {Object} options Configuration options for getting the block. + * @param {number} [options.rowIndex] Row index of the block. + * @return {import('react-test-renderer').ReactTestInstance} Block instance. */ export const getInnerBlock = ( parentBlock, diff --git a/test/native/integration-test-helpers/rich-text-paste.js b/test/native/integration-test-helpers/rich-text-paste.js index c84acaa9e9d60..d2c01ed2fb5a7 100644 --- a/test/native/integration-test-helpers/rich-text-paste.js +++ b/test/native/integration-test-helpers/rich-text-paste.js @@ -6,10 +6,10 @@ import { fireEvent } from '@testing-library/react-native'; /** * Paste content into a RichText component. * - * @param {HTMLElement} richText RichText test instance. - * @param {Object} content Content to paste. - * @param {string} content.text Text format of the content. - * @param {string} [content.html] HTML format of the content. If not provided, text format will be used. + * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. + * @param {Object} content Content to paste. + * @param {string} content.text Text format of the content. + * @param {string} [content.html] HTML format of the content. If not provided, text format will be used. */ export const pasteIntoRichText = ( richText, { text, html } ) => { fireEvent( richText, 'focus' ); diff --git a/test/native/integration-test-helpers/rich-text-select-range.js b/test/native/integration-test-helpers/rich-text-select-range.js index 7b2eeb2cae8c3..0e44a5af3a1d2 100644 --- a/test/native/integration-test-helpers/rich-text-select-range.js +++ b/test/native/integration-test-helpers/rich-text-select-range.js @@ -6,9 +6,9 @@ import { typeInRichText } from './rich-text-type'; /** * Select a range within a RichText component. * - * @param {HTMLElement} richText RichText test instance. - * @param {number} start Selection start position. - * @param {number} end Selection end position. + * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. + * @param {number} start Selection start position. + * @param {number} end Selection end position. */ export const selectRangeInRichText = ( richText, start, end = start ) => { if ( typeof start !== 'number' ) { diff --git a/test/native/integration-test-helpers/rich-text-type.js b/test/native/integration-test-helpers/rich-text-type.js index e6a724d8387f0..3be020325c2ed 100644 --- a/test/native/integration-test-helpers/rich-text-type.js +++ b/test/native/integration-test-helpers/rich-text-type.js @@ -16,13 +16,13 @@ function insertTextAtPosition( text, newText, start, end ) { /** * Changes the text and selection of a RichText component. * - * @param {HTMLElement} richText RichText test instance. - * @param {string} text Text to set. - * @param {Object} options Configuration options for selection. - * @param {number} [options.initialSelectionStart] Selection start position before the text is inserted. - * @param {number} [options.initialSelectionEnd] Selection end position before the text is inserted. - * @param {number} [options.finalSelectionStart] Selection start position after the text is inserted. - * @param {number} [options.finalSelectionEnd] Selection end position after the text is inserted. + * @param {import('react-test-renderer').ReactTestInstance} richText RichText test instance. + * @param {string} text Text to set. + * @param {Object} options Configuration options for selection. + * @param {number} [options.initialSelectionStart] Selection start position before the text is inserted. + * @param {number} [options.initialSelectionEnd] Selection end position before the text is inserted. + * @param {number} [options.finalSelectionStart] Selection start position after the text is inserted. + * @param {number} [options.finalSelectionEnd] Selection end position after the text is inserted. */ export const typeInRichText = ( richText, text, options = {} ) => { const currentValueSansOuterHtmlTags = stripOuterHtmlTags( diff --git a/test/native/integration-test-helpers/text-input-change-text.js b/test/native/integration-test-helpers/text-input-change-text.js index 2526b074d5f35..5e12a2602e9f3 100644 --- a/test/native/integration-test-helpers/text-input-change-text.js +++ b/test/native/integration-test-helpers/text-input-change-text.js @@ -6,8 +6,8 @@ import { fireEvent } from '@testing-library/react-native'; /** * Changes the text of a TextInput component. * - * @param {HTMLElement} textInput TextInput test instance. - * @param {string} text Text to be set. + * @param {import('react-test-renderer').ReactTestInstance} textInput TextInput test instance. + * @param {string} text Text to be set. */ export const changeTextOfTextInput = ( textInput, text ) => { fireEvent( textInput, 'focus' ); diff --git a/test/native/integration-test-helpers/transform-block.js b/test/native/integration-test-helpers/transform-block.js index 6afbae55ddf76..3feb6fbb32827 100644 --- a/test/native/integration-test-helpers/transform-block.js +++ b/test/native/integration-test-helpers/transform-block.js @@ -21,7 +21,7 @@ const apiFetchPromise = Promise.resolve( {} ); * @param {Object} [options] Configuration options for the transformation. * @param {number} [options.isMediaBlock] True if the block transformation will result in a media block. * @param {number} [options.hasInnerBlocks] True if the block transformation will result in a block that contains inner blocks. - * @return {HTMLElement} Block instance after the block transformation result. + * @return {import('react-test-renderer').ReactTestInstance} Block instance after the block transformation result. */ export const transformBlock = async ( screen, diff --git a/test/native/integration-test-helpers/trigger-block-list-layout.js b/test/native/integration-test-helpers/trigger-block-list-layout.js index b0a7caaf3fa17..5ccea7722cbf0 100644 --- a/test/native/integration-test-helpers/trigger-block-list-layout.js +++ b/test/native/integration-test-helpers/trigger-block-list-layout.js @@ -15,10 +15,10 @@ import { waitForStoreResolvers } from './wait-for-store-resolvers'; * case any of the inner elements use selectors that are associated with store * resolvers. * - * @param {HTMLElement} block Block test instance to trigger layout event. - * @param {Object} [options] Configuration options for the event. - * @param {number} [options.width] Width value to be passed to the event. - * @param {number} [options.blockListIndex] Block list index, for cases when there is more than one, like in inner blocks. + * @param {import('react-test-renderer').ReactTestInstance} block Block test instance to trigger layout event. + * @param {Object} [options] Configuration options for the event. + * @param {number} [options.width] Width value to be passed to the event. + * @param {number} [options.blockListIndex] Block list index, for cases when there is more than one, like in inner blocks. */ export const triggerBlockListLayout = async ( block, diff --git a/test/native/integration-test-helpers/wait-for-modal-visible.js b/test/native/integration-test-helpers/wait-for-modal-visible.js index c2a73a26e07f8..045b50a60cdbb 100644 --- a/test/native/integration-test-helpers/wait-for-modal-visible.js +++ b/test/native/integration-test-helpers/wait-for-modal-visible.js @@ -6,7 +6,7 @@ import { waitFor } from '@testing-library/react-native'; /** * Waits for a modal to be visible. * - * @param {HTMLElement} modalInstance Modal test instance. + * @param {import('react-test-renderer').ReactTestInstance} modalInstance Modal test instance. */ export const waitForModalVisible = async ( modalInstance ) => { return waitFor( () => diff --git a/test/native/integration/blocks-raw-handling.native.js b/test/native/integration/blocks-raw-handling.native.js index 5f21ca035fbf9..6fae0e72be0ca 100644 --- a/test/native/integration/blocks-raw-handling.native.js +++ b/test/native/integration/blocks-raw-handling.native.js @@ -187,7 +187,7 @@ describe( 'Blocks raw handling', () => { .join( '' ); expect( filtered ).toMatchInlineSnapshot( ` - "<ul><!-- wp:list-item --> + "<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -212,7 +212,7 @@ describe( 'Blocks raw handling', () => { .join( '' ); expect( filtered ).toMatchInlineSnapshot( ` - "<ul><!-- wp:list-item --> + "<ul class="wp-block-list"><!-- wp:list-item --> <li>one</li> <!-- /wp:list-item --> @@ -309,7 +309,7 @@ describe( 'Blocks raw handling', () => { it( 'should treat single list item as inline text', () => { const filtered = pasteHandler( { - HTML: '<ul><li>Some <strong>bold</strong> text.</li></ul>', + HTML: '<ul class="wp-block-list"><li>Some <strong>bold</strong> text.</li></ul>', plainText: 'Some <strong>bold</strong> text.\n', mode: 'AUTO', } ); @@ -320,7 +320,7 @@ describe( 'Blocks raw handling', () => { it( 'should treat multiple list items as a block', () => { const filtered = pasteHandler( { - HTML: '<ul><li>One</li><li>Two</li><li>Three</li></ul>', + HTML: '<ul class="wp-block-list"><li>One</li><li>Two</li><li>Three</li></ul>', plainText: 'One\nTwo\nThree\n', mode: 'AUTO', } ) @@ -328,7 +328,7 @@ describe( 'Blocks raw handling', () => { .join( '' ); expect( filtered ).toMatchInlineSnapshot( ` - "<ul><!-- wp:list-item --> + "<ul class="wp-block-list"><!-- wp:list-item --> <li>One</li> <!-- /wp:list-item --> diff --git a/test/native/matchers/to-be-visible.js b/test/native/matchers/to-be-visible.js index 3a9d46474cfc0..b2b19d239a856 100644 --- a/test/native/matchers/to-be-visible.js +++ b/test/native/matchers/to-be-visible.js @@ -38,7 +38,7 @@ function isElementVisible( element ) { * - it is not a "Modal" component or it does not have the prop "visible" set to "false". * - its ancestor elements are also visible. * - * @param {HTMLElement} element + * @param {import('react-test-renderer').ReactTestInstance} element * @return {boolean} True if the given element is visible. */ export function toBeVisible( element ) { diff --git a/test/performance/config/performance-reporter.ts b/test/performance/config/performance-reporter.ts index c82c092f30478..b449e36540404 100644 --- a/test/performance/config/performance-reporter.ts +++ b/test/performance/config/performance-reporter.ts @@ -13,7 +13,7 @@ import type { /** * Internal dependencies */ -import { average, median, minimum, maximum, round } from '../utils'; +import { average, median, round } from '../utils'; export interface WPRawPerformanceResults { timeToFirstByte: number[]; @@ -36,6 +36,11 @@ export interface WPRawPerformanceResults { loadPatterns: number[]; listViewOpen: number[]; navigate: number[]; + wpBeforeTemplate: number[]; + wpTemplate: number[]; + wpTotal: number[]; + wpMemoryUsage: number[]; + wpDbQueries: number[]; } export interface WPPerformanceResults { @@ -59,6 +64,11 @@ export interface WPPerformanceResults { loadPatterns?: number; listViewOpen?: number; navigate?: number; + wpBeforeTemplate?: number; + wpTemplate?: number; + wpTotal?: number; + wpMemoryUsage?: number; + wpDbQueries?: number; } /** @@ -92,6 +102,11 @@ export function curateResults( loadPatterns: average( results.loadPatterns ), listViewOpen: average( results.listViewOpen ), navigate: median( results.navigate ), + wpBeforeTemplate: median( results.wpBeforeTemplate ), + wpTemplate: median( results.wpTemplate ), + wpTotal: median( results.wpTotal ), + wpMemoryUsage: median( results.wpMemoryUsage ), + wpDbQueries: median( results.wpDbQueries ), }; return ( diff --git a/test/performance/specs/front-end-block-theme.spec.js b/test/performance/specs/front-end-block-theme.spec.js index ca48535a21a46..8a0f7b6cf391a 100644 --- a/test/performance/specs/front-end-block-theme.spec.js +++ b/test/performance/specs/front-end-block-theme.spec.js @@ -52,6 +52,13 @@ test.describe( 'Front End Performance', () => { results.largestContentfulPaint.push( lcp ); results.timeToFirstByte.push( ttfb ); results.lcpMinusTtfb.push( lcp - ttfb ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( serverTiming ) ) { + results[ key ] ??= []; + results[ key ].push( value ); + } } } ); } diff --git a/test/performance/specs/front-end-classic-theme.spec.js b/test/performance/specs/front-end-classic-theme.spec.js index 0b6c3ec22c046..64929086079fe 100644 --- a/test/performance/specs/front-end-classic-theme.spec.js +++ b/test/performance/specs/front-end-classic-theme.spec.js @@ -51,6 +51,13 @@ test.describe( 'Front End Performance', () => { results.largestContentfulPaint.push( lcp ); results.timeToFirstByte.push( ttfb ); results.lcpMinusTtfb.push( lcp - ttfb ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( serverTiming ) ) { + results[ key ] ??= []; + results[ key ].push( value ); + } } } ); } diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 30baa8b799390..37e38d0d9ec47 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -89,6 +89,15 @@ test.describe( 'Post Editor Performance', () => { } } ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( + serverTiming + ) ) { + results[ key ] ??= []; + results[ key ].push( value ); + } } } ); } @@ -659,7 +668,7 @@ test.describe( 'Post Editor Performance', () => { const startTime = performance.now(); - await page.getByText( 'Test' ).click(); + await page.getByRole( 'tab', { name: 'Test' } ).click(); await Promise.all( testPatterns.map( async ( pattern ) => { diff --git a/test/performance/specs/site-editor.spec.js b/test/performance/specs/site-editor.spec.js index c4c83f0d4c140..e8a4e3529334d 100644 --- a/test/performance/specs/site-editor.spec.js +++ b/test/performance/specs/site-editor.spec.js @@ -101,6 +101,15 @@ test.describe( 'Site Editor Performance', () => { } } ); + + const serverTiming = await metrics.getServerTiming(); + + for ( const [ key, value ] of Object.entries( + serverTiming + ) ) { + results[ key ] ??= []; + results[ key ].push( value ); + } } } ); } diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/FunctionCommentSinceTagSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/FunctionCommentSinceTagSniff.php deleted file mode 100644 index 54b8b367560fc..0000000000000 --- a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/FunctionCommentSinceTagSniff.php +++ /dev/null @@ -1,162 +0,0 @@ -<?php -/** - * Gutenberg Coding Standards. - * - * @package gutenberg/gutenberg-coding-standards - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT - */ - -namespace GutenbergCS\Gutenberg\Sniffs\Commenting; - -use PHP_CodeSniffer\Files\File; -use PHP_CodeSniffer\Sniffs\Sniff; -use PHP_CodeSniffer\Util\Tokens; - -/** - * This sniff ensures that PHP functions have a valid `@since` tag in the docblock. - * The sniff skips checking files in __experimental block-library blocks. - */ -class FunctionCommentSinceTagSniff implements Sniff { - - /** - * This property is used to store results returned - * by the static::is_experimental_block() method. - * - * @var array - */ - private static $cache = array(); - - /** - * Returns an array of tokens this test wants to listen for. - * - * @return array<int|string> - */ - public function register() { - return array( T_FUNCTION ); - } - - /** - * Processes the tokens that this sniff is interested in. - * - * @param File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the stack passed in $tokens. - */ - public function process( File $phpcsFile, $stackPtr ) { - if ( static::is_experimental_block( $phpcsFile ) ) { - // The "@since" tag is not required for experimental blocks since they are not yet included in WordPress Core. - return; - } - - $tokens = $phpcsFile->getTokens(); - $function_name = $phpcsFile->getDeclarationName( $stackPtr ); - - $wrapping_tokens_to_check = array( - T_CLASS, - T_INTERFACE, - T_TRAIT, - ); - - foreach ( $wrapping_tokens_to_check as $wrapping_token_to_check ) { - if ( false !== $phpcsFile->getCondition( $stackPtr, $wrapping_token_to_check, false ) ) { - // This sniff only processes functions, not class methods. - return; - } - } - - $missing_since_tag_error_message = sprintf( '@since tag is missing for the "%s()" function.', $function_name ); - - // All these tokens could be present before the docblock. - $tokens_before_the_docblock = array( - T_PUBLIC, - T_PROTECTED, - T_PRIVATE, - T_STATIC, - T_FINAL, - T_ABSTRACT, - T_WHITESPACE, - ); - - $doc_block_end_token = $phpcsFile->findPrevious( $tokens_before_the_docblock, ( $stackPtr - 1 ), null, true, null, true ); - if ( ( false === $doc_block_end_token ) || ( T_DOC_COMMENT_CLOSE_TAG !== $tokens[ $doc_block_end_token ]['code'] ) ) { - $phpcsFile->addError( $missing_since_tag_error_message, $stackPtr, 'MissingSinceTag' ); - return; - } - - // The sniff intentionally doesn't check if the docblock has a valid open tag. - // Its only job is to make sure that the @since tag is present and has a valid version value. - $doc_block_start_token = $phpcsFile->findPrevious( Tokens::$commentTokens, ( $doc_block_end_token - 1 ), null, true, null, true ); - if ( false === $doc_block_start_token ) { - $phpcsFile->addError( $missing_since_tag_error_message, $stackPtr, 'MissingSinceTag' ); - return; - } - - // This is the first non-docblock token, so the next token should be used. - ++$doc_block_start_token; - - $since_tag_token = $phpcsFile->findNext( T_DOC_COMMENT_TAG, $doc_block_start_token, $doc_block_end_token, false, '@since', true ); - if ( false === $since_tag_token ) { - $phpcsFile->addError( $missing_since_tag_error_message, $stackPtr, 'MissingSinceTag' ); - return; - } - - $version_token = $phpcsFile->findNext( T_DOC_COMMENT_WHITESPACE, $since_tag_token + 1, null, true, null, true ); - if ( ( false === $version_token ) || ( T_DOC_COMMENT_STRING !== $tokens[ $version_token ]['code'] ) ) { - $phpcsFile->addError( $missing_since_tag_error_message, $since_tag_token, 'MissingSinceTag' ); - return; - } - - $version_value = $tokens[ $version_token ]['content']; - - if ( version_compare( $version_value, '0.0.1', '>=' ) ) { - // Validate the version value. - return; - } - - $phpcsFile->addError( - 'Invalid @since version value for the "%s()" function: "%s". Version value must be greater than or equal to 0.0.1.', - $version_token, - 'InvalidSinceTagVersionValue', - array( - $function_name, - $version_value, - ) - ); - } - - /** - * Checks if the current block is experimental. - * - * @param File $phpcsFile The file being scanned. - * @return bool Returns true if the current block is experimental. - */ - private static function is_experimental_block( File $phpcsFile ) { - $block_json_filepath = dirname( $phpcsFile->getFilename() ) . DIRECTORY_SEPARATOR . 'block.json'; - - if ( isset( static::$cache[ $block_json_filepath ] ) ) { - return static::$cache[ $block_json_filepath ]; - } - - if ( ! is_file( $block_json_filepath ) || ! is_readable( $block_json_filepath ) ) { - static::$cache[ $block_json_filepath ] = false; - return static::$cache[ $block_json_filepath ]; - } - - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- this Composer package doesn't depend on WordPress. - $block_metadata = file_get_contents( $block_json_filepath ); - if ( false === $block_metadata ) { - static::$cache[ $block_json_filepath ] = false; - return static::$cache[ $block_json_filepath ]; - } - - $block_metadata = json_decode( $block_metadata, true ); - if ( ! is_array( $block_metadata ) ) { - static::$cache[ $block_json_filepath ] = false; - return static::$cache[ $block_json_filepath ]; - } - - $experimental_flag = '__experimental'; - static::$cache[ $block_json_filepath ] = array_key_exists( $experimental_flag, $block_metadata ) && ( false !== $block_metadata[ $experimental_flag ] ); - return static::$cache[ $block_json_filepath ]; - } -} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php new file mode 100644 index 0000000000000..f216f4f681f0e --- /dev/null +++ b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php @@ -0,0 +1,617 @@ +<?php +/** + * Gutenberg Coding Standards. + * + * @package gutenberg/gutenberg-coding-standards + * @link https://github.com/WordPress/gutenberg + * @license https://opensource.org/licenses/MIT MIT + */ + +namespace GutenbergCS\Gutenberg\Sniffs\Commenting; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\FunctionDeclarations; +use PHPCSUtils\Utils\GetTokensAsString; +use PHPCSUtils\Utils\ObjectDeclarations; +use PHPCSUtils\Utils\Scopes; +use PHPCSUtils\Utils\Variables; + +/** + * This sniff verifies the presence of valid `@since` tags in the docblocks of various PHP structures + * and WordPress hooks. Supported structures include classes, interfaces, traits, enums, functions, methods and properties. + * Files located within the __experimental block of the block-library are excluded from checks. + */ +class SinceTagSniff implements Sniff { + + /** + * Disable the check for functions with a lower visibility than the value given. + * + * Allowed values are public, protected, and private. + * + * @var string + */ + public $minimumVisibility = 'private'; + + /** + * A map of tokens representing an object-oriented programming structure to their human-readable names. + * This map helps in identifying different OO structures such as classes, interfaces, traits, and enums. + * + * @var array + */ + protected static $oo_tokens = array( + T_CLASS => array( + 'name' => 'class', + ), + T_INTERFACE => array( + 'name' => 'interface', + ), + T_TRAIT => array( + 'name' => 'trait', + ), + T_ENUM => array( + 'name' => 'enum', + ), + ); + + /** + * This property is used to store results returned + * by the static::is_experimental_block() method. + * + * @var array + */ + protected static $cache = array(); + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array<int|string> + */ + public function register() { + return array_merge( + array( + T_FUNCTION, + T_VARIABLE, + T_STRING, + ), + array_keys( static::$oo_tokens ) + ); + } + + /** + * Processes the tokens that this sniff is interested in. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + */ + public function process( File $phpcsFile, $stackPtr ) { + if ( static::is_experimental_block( $phpcsFile ) ) { + // The "@since" tag is not required for experimental blocks since they are not yet included in WordPress Core. + return; + } + + $tokens = $phpcsFile->getTokens(); + $token = $tokens[ $stackPtr ]; + + if ( 'T_FUNCTION' === $token['type'] ) { + $this->process_function_token( $phpcsFile, $stackPtr ); + return; + } + + if ( isset( static::$oo_tokens[ $token['code'] ] ) ) { + $this->process_oo_token( $phpcsFile, $stackPtr ); + return; + } + + if ( 'T_STRING' === $token['type'] && static::is_function_call( $phpcsFile, $stackPtr ) ) { + $this->process_hook( $phpcsFile, $stackPtr ); + return; + } + + if ( 'T_VARIABLE' === $token['type'] && Scopes::isOOProperty( $phpcsFile, $stackPtr ) ) { + $this->process_property_token( $phpcsFile, $stackPtr ); + } + } + + /** + * Processes a token representing a function call that invokes a WordPress hook, + * checking for a missing `@since` tag in its docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the hook token in the stack. + */ + protected function process_hook( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + + // The content of the current token. + $hook_function = $tokens[ $stack_pointer ]['content']; + + $hook_invocation_functions = array( + 'do_action', + 'do_action_ref_array', + 'do_action_deprecated', + 'apply_filters', + 'apply_filters_ref_array', + 'apply_filters_deprecated', + ); + + // Check if the current token content is one of the filter functions. + if ( ! in_array( $hook_function, $hook_invocation_functions, true ) ) { + // Not a hook. + return; + } + + $error_message_data = array( $hook_function ); + + $violation_codes = static::get_violation_codes( 'Hook' ); + + $docblock = static::find_hook_docblock( $phpcs_file, $stack_pointer ); + + $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); + if ( empty( $version_tags ) ) { + if ( false !== $docblock ) { + $docblock_content = GetTokensAsString::compact( $phpcs_file, $docblock['start_token'], $docblock['end_token'], false ); + if ( false !== stripos( $docblock_content, 'This filter is documented in ' ) ) { + $hook_documented_elsewhere = true; + } + } + + if ( empty( $hook_documented_elsewhere ) ) { + $phpcs_file->addError( + 'Missing @since tag for the "%s()" hook function.', + $stack_pointer, + $violation_codes['missing_since_tag'], + $error_message_data + ); + } + + return; + } + + foreach ( $version_tags as $since_tag_token => $version_value_token ) { + if ( null === $version_value_token ) { + $phpcs_file->addError( + 'Missing @since tag version value for the "%s()" hook function.', + $since_tag_token, + $violation_codes['missing_version_value'], + $error_message_data + ); + continue; + } + + $version_value = $tokens[ $version_value_token ]['content']; + + if ( static::validate_version( $version_value ) ) { + continue; + } + + $phpcs_file->addError( + 'Invalid @since version value for the "%s()" hook function: "%s". Version value must be greater than or equal to 0.0.1.', + $version_value_token, + $violation_codes['invalid_version_value'], + array_merge( $error_message_data, array( $version_value ) ) + ); + } + } + + /** + * Processes a token representing an object-oriented programming structure + * like a class, interface, trait, or enum to check for a missing `@since` tag in its docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the OO token in the stack. + */ + protected function process_oo_token( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + $token_type = static::$oo_tokens[ $tokens[ $stack_pointer ]['code'] ]['name']; + + $token_name = ObjectDeclarations::getName( $phpcs_file, $stack_pointer ); + $error_message_data = array( + $token_name, + $token_type, + ); + + $violation_codes = static::get_violation_codes( ucfirst( $token_type ) ); + + $docblock = static::find_docblock( $phpcs_file, $stack_pointer ); + + $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); + if ( empty( $version_tags ) ) { + $phpcs_file->addError( + 'Missing @since tag for the "%s" %s.', + $stack_pointer, + $violation_codes['missing_since_tag'], + $error_message_data + ); + return; + } + + foreach ( $version_tags as $since_tag_token => $version_value_token ) { + if ( null === $version_value_token ) { + $phpcs_file->addError( + 'Missing @since tag version value for the "%s" %s.', + $since_tag_token, + $violation_codes['missing_version_value'], + $error_message_data + ); + continue; + } + + $version_value = $tokens[ $version_value_token ]['content']; + + if ( static::validate_version( $version_value ) ) { + continue; + } + + $phpcs_file->addError( + 'Invalid @since version value for the "%s" %s: "%s". Version value must be greater than or equal to 0.0.1.', + $version_value_token, + $violation_codes['invalid_version_value'], + array_merge( $error_message_data, array( $version_value ) ) + ); + } + } + + /** + * Processes a token representing an object-oriented property to check for a missing @since tag in its docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the object-oriented property token in the stack. + */ + protected function process_property_token( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + + $property_name = $tokens[ $stack_pointer ]['content']; + $oo_token = Scopes::validDirectScope( $phpcs_file, $stack_pointer, Collections::ooPropertyScopes() ); + $class_name = ObjectDeclarations::getName( $phpcs_file, $oo_token ); + + $visibility = Variables::getMemberProperties( $phpcs_file, $stack_pointer )['scope']; + if ( $this->check_below_minimum_visibility( $visibility ) ) { + return; + } + + $violation_codes = static::get_violation_codes( 'Property' ); + + $error_message_data = array( + $class_name, + $property_name, + ); + + $docblock = static::find_docblock( $phpcs_file, $stack_pointer ); + + $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); + if ( empty( $version_tags ) ) { + $phpcs_file->addError( + 'Missing @since tag for the "%s::%s" property.', + $stack_pointer, + $violation_codes['missing_since_tag'], + $error_message_data + ); + return; + } + + foreach ( $version_tags as $since_tag_token => $version_value_token ) { + if ( null === $version_value_token ) { + $phpcs_file->addError( + 'Missing @since tag version value for the "%s::%s" property.', + $since_tag_token, + $violation_codes['missing_version_value'], + $error_message_data + ); + continue; + } + + $version_value = $tokens[ $version_value_token ]['content']; + + if ( static::validate_version( $version_value ) ) { + continue; + } + + $phpcs_file->addError( + 'Invalid @since version value for the "%s::%s" property: "%s". Version value must be greater than or equal to 0.0.1.', + $version_value_token, + $violation_codes['invalid_version_value'], + array_merge( $error_message_data, array( $version_value ) ) + ); + } + } + + /** + * Processes a T_FUNCTION token to check for a missing @since tag in its docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the T_FUNCTION token in the stack. + */ + protected function process_function_token( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + + $oo_token = Scopes::validDirectScope( $phpcs_file, $stack_pointer, Tokens::$ooScopeTokens ); + $function_name = ObjectDeclarations::getName( $phpcs_file, $stack_pointer ); + + $token_type = 'function'; + if ( Scopes::isOOMethod( $phpcs_file, $stack_pointer ) ) { + $visibility = FunctionDeclarations::getProperties( $phpcs_file, $stack_pointer )['scope']; + if ( $this->check_below_minimum_visibility( $visibility ) ) { + return; + } + + $function_name = ObjectDeclarations::getName( $phpcs_file, $oo_token ) . '::' . $function_name; + $token_type = 'method'; + } + + $violation_codes = static::get_violation_codes( ucfirst( $token_type ) ); + + $error_message_data = array( + $function_name, + $token_type, + ); + + $docblock = static::find_docblock( $phpcs_file, $stack_pointer ); + + $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); + if ( empty( $version_tags ) ) { + $phpcs_file->addError( + 'Missing @since tag for the "%s()" %s.', + $stack_pointer, + $violation_codes['missing_since_tag'], + $error_message_data + ); + return; + } + + foreach ( $version_tags as $since_tag_token => $version_value_token ) { + if ( null === $version_value_token ) { + $phpcs_file->addError( + 'Missing @since tag version value for the "%s()" %s.', + $since_tag_token, + $violation_codes['missing_version_value'], + $error_message_data + ); + continue; + } + + $version_value = $tokens[ $version_value_token ]['content']; + + if ( static::validate_version( $version_value ) ) { + continue; + } + + $phpcs_file->addError( + 'Invalid @since version value for the "%s()" %s: "%s". Version value must be greater than or equal to 0.0.1.', + $version_value_token, + $violation_codes['invalid_version_value'], + array_merge( $error_message_data, array( $version_value ) ) + ); + } + } + + /** + * Validates the version value. + * + * @param string $version The version value being checked. + * @return bool True if the version value is valid. + */ + protected static function validate_version( $version ) { + $matches = array(); + if ( 1 === preg_match( '/^MU \((?<version>.+)\)/', $version, $matches ) ) { + $version = $matches['version']; + } + + return version_compare( $version, '0.0.1', '>=' ); + } + + + /** + * Returns violation codes for a specific token type. + * + * @param string $token_type The type of token (e.g., Function, Property) to retrieve violation codes for. + * @return array An array containing violation codes for missing since tag, missing version value, and invalid version value. + */ + protected static function get_violation_codes( $token_type ) { + return array( + 'missing_since_tag' => 'Missing' . $token_type . 'SinceTag', + 'missing_version_value' => 'Missing' . $token_type . 'VersionValue', + 'invalid_version_value' => 'Invalid' . $token_type . 'VersionValue', + ); + } + + /** + * Checks if the provided visibility level is below the set minimum visibility level. + * + * @param string $visibility The visibility level to check. + * @return bool Returns true if the provided visibility level is below the minimum visibility level, false otherwise. + */ + protected function check_below_minimum_visibility( $visibility ) { + if ( 'public' === $this->minimumVisibility && in_array( $visibility, array( 'protected', 'private' ), true ) ) { + return true; + } + + if ( 'protected' === $this->minimumVisibility && 'private' === $visibility ) { + return true; + } + + return false; + } + + /** + * Finds the docblock associated with a hook, starting from a specified position in the token stack. + * Since a line containing a hook can include any type of tokens, this method backtracks through the tokens + * to locate the first token on the current line. This token is then used as the starting point for searching the docblock. + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position to start looking for the docblock. + * @return array|false An associative array containing the start and end tokens of the docblock, or false if not found. + */ + protected static function find_hook_docblock( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + $current_line = $tokens[ $stack_pointer ]['line']; + + for ( $i = $stack_pointer; $i >= 0; $i-- ) { + if ( $tokens[ $i ]['line'] < $current_line ) { + // The previous token is on the previous line, so the current token is the first on the line. + return static::find_docblock( $phpcs_file, $i + 1 ); + } + } + + return static::find_docblock( $phpcs_file, 0 ); + } + + /** + * Determines if a T_STRING token represents a function call. + * The implementation was copied from PHPCompatibility\Sniffs\Extensions\RemovedExtensionsSniff::process(). + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position of the T_STRING token in question. + * @return bool True if the token represents a function call, false otherwise. + */ + protected static function is_function_call( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + + // Find the next non-empty token. + $open_bracket = $phpcs_file->findNext( Tokens::$emptyTokens, ( $stack_pointer + 1 ), null, true ); + + if ( T_OPEN_PARENTHESIS !== $tokens[ $open_bracket ]['code'] ) { + // Not a function call. + return false; + } + + if ( false === isset( $tokens[ $open_bracket ]['parenthesis_closer'] ) ) { + // Not a function call. + return false; + } + + // Find the previous non-empty token. + $search = Tokens::$emptyTokens; + $search[] = T_BITWISE_AND; + $previous = $phpcs_file->findPrevious( $search, ( $stack_pointer - 1 ), null, true ); + + $previous_tokens_to_ignore = array( + T_FUNCTION, // Function declaration. + T_NEW, // Creating an object. + T_OBJECT_OPERATOR, // Calling an object. + ); + + return ! in_array( $tokens[ $previous ]['code'], $previous_tokens_to_ignore, true ); + } + + /** + * Finds the docblock preceding a specified position (stack pointer) in a given PHP file. + * The implementation was copied from PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff::process(). + * + * @param File $phpcs_file The file being scanned. + * @param int $stack_pointer The position (stack pointer) in the token stack from which to start searching backwards. + * @return array|false An associative array containing the start and end tokens of the docblock, or false if not found. + */ + protected static function find_docblock( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); + $ignore = Tokens::$methodPrefixes; + $ignore[ T_WHITESPACE ] = T_WHITESPACE; + + for ( $comment_end = ( $stack_pointer - 1 ); $comment_end >= 0; $comment_end-- ) { + if ( isset( $ignore[ $tokens[ $comment_end ]['code'] ] ) ) { + continue; + } + + if ( T_ATTRIBUTE_END === $tokens[ $comment_end ]['code'] + && isset( $tokens[ $comment_end ]['attribute_opener'] ) + ) { + $comment_end = $tokens[ $comment_end ]['attribute_opener']; + continue; + } + + break; + } + + if ( $tokens[ $comment_end ]['code'] === T_COMMENT ) { + // Inline comments might just be closing comments for + // control structures or functions instead of function comments + // using the wrong comment type. If there is other code on the line, + // assume they relate to that code. + $previous = $phpcs_file->findPrevious( $ignore, ( $comment_end - 1 ), null, true ); + if ( false !== $previous && $tokens[ $previous ]['line'] === $tokens[ $comment_end ]['line'] ) { + $comment_end = $previous; + } + } + + if ( T_DOC_COMMENT_CLOSE_TAG !== $tokens[ $comment_end ]['code'] ) { + // Only "/**" style comments are supported. + return false; + } + + return array( + 'start_token' => $tokens[ $comment_end ]['comment_opener'], + 'end_token' => $comment_end, + ); + } + + /** + * Searches for @since values within a docblock. + * + * @param File $phpcs_file The file being scanned. + * @param array|false $docblock An associative array containing the start and end tokens of the docblock, or false if not exists. + * @return array Returns an array of "@since" tokens and their corresponding value tokens. + */ + protected static function parse_since_tags( File $phpcs_file, $docblock ) { + $version_tags = array(); + + if ( false === $docblock ) { + return $version_tags; + } + + $tokens = $phpcs_file->getTokens(); + + for ( $i = $docblock['start_token'] + 1; $i < $docblock['end_token']; $i++ ) { + if ( ! ( T_DOC_COMMENT_TAG === $tokens[ $i ]['code'] && '@since' === $tokens[ $i ]['content'] ) ) { + continue; + } + + $version_token = $phpcs_file->findNext( T_DOC_COMMENT_WHITESPACE, $i + 1, $docblock['end_token'], true, null, true ); + if ( ( false === $version_token ) || ( T_DOC_COMMENT_STRING !== $tokens[ $version_token ]['code'] ) ) { + $version_tags[ $i ] = null; + continue; + } + + $version_tags[ $i ] = $version_token; + } + + return $version_tags; + } + + /** + * Checks if the current block is experimental. + * + * @param File $phpcs_file The file being scanned. + * @return bool Returns true if the current block is experimental. + */ + protected static function is_experimental_block( File $phpcs_file ) { + $block_json_filepath = dirname( $phpcs_file->getFilename() ) . DIRECTORY_SEPARATOR . 'block.json'; + + if ( isset( static::$cache[ $block_json_filepath ] ) ) { + return static::$cache[ $block_json_filepath ]; + } + + if ( ! is_file( $block_json_filepath ) || ! is_readable( $block_json_filepath ) ) { + static::$cache[ $block_json_filepath ] = false; + return static::$cache[ $block_json_filepath ]; + } + + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- this Composer package doesn't depend on WordPress. + $block_metadata = file_get_contents( $block_json_filepath ); + if ( false === $block_metadata ) { + static::$cache[ $block_json_filepath ] = false; + return static::$cache[ $block_json_filepath ]; + } + + $block_metadata = json_decode( $block_metadata, true ); + if ( ! is_array( $block_metadata ) ) { + static::$cache[ $block_json_filepath ] = false; + return static::$cache[ $block_json_filepath ]; + } + + $experimental_flag = '__experimental'; + static::$cache[ $block_json_filepath ] = array_key_exists( $experimental_flag, $block_metadata ) && ( false !== $block_metadata[ $experimental_flag ] ); + return static::$cache[ $block_json_filepath ]; + } +} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php new file mode 100644 index 0000000000000..08838ce412fc3 --- /dev/null +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php @@ -0,0 +1,85 @@ +<?php +/** + * An abstract class that all sniff unit tests must extend. + * + * @package gutenberg-coding-standards/gbc + * @link https://github.com/WordPress/gutenberg + * @license https://opensource.org/licenses/MIT MIT + */ + +namespace GutenbergCS\Gutenberg\Tests; + +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest as BaseAbstractSniffUnitTest; +use PHP_CodeSniffer\Ruleset; +use PHP_CodeSniffer\Sniffs\Sniff; + +abstract class AbstractSniffUnitTest extends BaseAbstractSniffUnitTest { + + /** + * Holds the original Ruleset instance. + * + * @var Ruleset + */ + protected static $original_ruleset; + + /** + * This method resets the 'Gutenberg' ruleset in the $GLOBALS['PHP_CODESNIFFER_RULESETS'] + * to its original state. + */ + public static function tearDownAfterClass() { + parent::tearDownAfterClass(); + + $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = static::$original_ruleset; + static::$original_ruleset = null; + } + + /** + * Sets the parameters for the sniff. + * + * @throws RuntimeException If unable to set the ruleset parameters required for the test. + * + * @param Sniff $sniff The sniff being tested. + */ + abstract protected function set_sniff_parameters( Sniff $sniff ); + + /** + * Returns the fully qualified class name (FQCN) of the sniff. + * + * @return string The fully qualified class name of the sniff. + */ + abstract protected function get_sniff_fqcn(); + + /** + * Prepares the environment before executing tests. This is needed since + * PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest doesn't apply + * sniff properties from the Gutenberg/ruleset.xml file. + * + * @param string $filename The name of the file being tested. + * @param Config $config The config data for the run. + */ + public function setCliValues( $filename, $config ) { + parent::setCliValues( $filename, $config ); + + $error_message = 'Cannot set sniff parameters required for the unit test.'; + if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] ) + || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset ) + ) { + throw new \RuntimeException( $error_message ); + } + + // Backup the original Ruleset instance. + static::$original_ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg']; + + $current_ruleset = clone static::$original_ruleset; + $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = $current_ruleset; + + $sniff_fqcn = $this->get_sniff_fqcn(); + if ( ! isset( $current_ruleset->sniffs[ $sniff_fqcn ] ) ) { + throw new \RuntimeException( $error_message ); + } + + $sniff = $current_ruleset->sniffs[ $sniff_fqcn ]; + $this->set_sniff_parameters( $sniff ); + } +} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php index 5073cea9ecf06..8026e88f1d945 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php @@ -10,22 +10,14 @@ namespace GutenbergCS\Gutenberg\Tests\CodeAnalysis; use GutenbergCS\Gutenberg\Sniffs\CodeAnalysis\ForbiddenFunctionsAndClassesSniff; -use PHP_CodeSniffer\Config; -use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use PHP_CodeSniffer\Ruleset; +use GutenbergCS\Gutenberg\Tests\AbstractSniffUnitTest; +use PHP_CodeSniffer\Sniffs\Sniff; /** * Unit test class for the ForbiddenFunctionsAndClassesSniff sniff. */ final class ForbiddenFunctionsAndClassesUnitTest extends AbstractSniffUnitTest { - /** - * Holds the original Ruleset instance. - * - * @var Ruleset - */ - private static $original_ruleset; - /** * Returns the lines where errors should occur. * @@ -73,50 +65,22 @@ public function getWarningList() { } /** + * Returns the fully qualified class name (FQCN) of the sniff. * - * This method resets the 'Gutenberg' ruleset in the $GLOBALS['PHP_CODESNIFFER_RULESETS'] - * to its original state. + * @return string The fully qualified class name of the sniff. */ - public static function tearDownAfterClass() { - parent::tearDownAfterClass(); - - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = self::$original_ruleset; - self::$original_ruleset = null; + protected function get_sniff_fqcn() { + return ForbiddenFunctionsAndClassesSniff::class; } - /** - * Prepares the environment before executing tests. Specifically, sets prefixes for the - * ForbiddenFunctionsAndClassesSniff sniff.This is needed since AbstractSniffUnitTest class - * doesn't apply sniff properties from the Gutenberg/ruleset.xml file. + * Sets the parameters for the sniff. * - * @param string $filename The name of the file being tested. - * @param Config $config The config data for the run. + * @throws RuntimeException If unable to set the ruleset parameters required for the test. * - * @return void + * @param Sniff $sniff The sniff being tested. */ - public function setCliValues( $filename, $config ) { - parent::setCliValues( $filename, $config ); - - if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] ) - || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - // Backup the original Ruleset instance. - self::$original_ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg']; - - $current_ruleset = clone self::$original_ruleset; - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = $current_ruleset; - - if ( ! isset( $current_ruleset->sniffs[ ForbiddenFunctionsAndClassesSniff::class ] ) - || ( ! $current_ruleset->sniffs[ ForbiddenFunctionsAndClassesSniff::class ] instanceof ForbiddenFunctionsAndClassesSniff ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - $sniff = $current_ruleset->sniffs[ ForbiddenFunctionsAndClassesSniff::class ]; + public function set_sniff_parameters( Sniff $sniff ) { $sniff->forbidden_functions = array( '[Gg]utenberg.*', ); diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php index fdd5c07c8cb59..652f6b735378c 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php @@ -10,22 +10,14 @@ namespace GutenbergCS\Gutenberg\Tests\CodeAnalysis; use GutenbergCS\Gutenberg\Sniffs\CodeAnalysis\GuardedFunctionAndClassNamesSniff; -use PHP_CodeSniffer\Config; -use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use PHP_CodeSniffer\Ruleset; +use GutenbergCS\Gutenberg\Tests\AbstractSniffUnitTest; +use PHP_CodeSniffer\Sniffs\Sniff; /** * Unit test class for the GuardedFunctionAndClassNames sniff. */ final class GuardedFunctionAndClassNamesUnitTest extends AbstractSniffUnitTest { - /** - * Holds the original Ruleset instance. - * - * @var Ruleset - */ - private static $original_ruleset; - /** * Returns the lines where errors should occur. * @@ -50,50 +42,22 @@ public function getWarningList() { } /** + * Returns the fully qualified class name (FQCN) of the sniff. * - * This method resets the 'Gutenberg' ruleset in the $GLOBALS['PHP_CODESNIFFER_RULESETS'] - * to its original state. + * @return string The fully qualified class name of the sniff. */ - public static function tearDownAfterClass() { - parent::tearDownAfterClass(); - - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = self::$original_ruleset; - self::$original_ruleset = null; + protected function get_sniff_fqcn() { + return GuardedFunctionAndClassNamesSniff::class; } - /** - * Prepares the environment before executing tests. Specifically, sets prefixes for the - * GuardedFunctionAndClassNames sniff.This is needed since AbstractSniffUnitTest class - * doesn't apply sniff properties from the Gutenberg/ruleset.xml file. + * Sets the parameters for the sniff. * - * @param string $filename The name of the file being tested. - * @param Config $config The config data for the run. + * @throws RuntimeException If unable to set the ruleset parameters required for the test. * - * @return void + * @param Sniff $sniff The sniff being tested. */ - public function setCliValues( $filename, $config ) { - parent::setCliValues( $filename, $config ); - - if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] ) - || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - // Backup the original Ruleset instance. - self::$original_ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg']; - - $current_ruleset = clone self::$original_ruleset; - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = $current_ruleset; - - if ( ! isset( $current_ruleset->sniffs[ GuardedFunctionAndClassNamesSniff::class ] ) - || ( ! $current_ruleset->sniffs[ GuardedFunctionAndClassNamesSniff::class ] instanceof GuardedFunctionAndClassNamesSniff ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - $sniff = $current_ruleset->sniffs[ GuardedFunctionAndClassNamesSniff::class ]; + public function set_sniff_parameters( Sniff $sniff ) { $sniff->functionsWhiteList = array( '/^_?gutenberg.+/', ); diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.inc b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.inc deleted file mode 100644 index 43d11694bb25e..0000000000000 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.inc +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * @since 1.2.3 - */ -function foo() { -} - -/** - * @since invalid value - */ -function bar() { -} - -$result = array_map( function ( $value ) { - return $value * 2; // Doubling each value -}, array( 1, 2, 3, 4, 5 ) ); - -/** - * @since 0.0 - */ -function qux() { -} - -function spam() { -} - -class Foo { - public function bar() { - } -} - -interface Bar { - public function bar(); -} - -trait Baz { - public function bar() { - } -} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.php deleted file mode 100644 index b41e794d072fd..0000000000000 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/FunctionCommentSinceTagUnitTest.php +++ /dev/null @@ -1,42 +0,0 @@ -<?php -/** - * Unit test class for Gutenberg Coding Standard. - * - * @package gutenberg-coding-standards/gbc - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT - */ - -namespace GutenbergCS\Gutenberg\Tests\Commenting; - -use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use PHP_CodeSniffer\Ruleset; - -/** - * Unit test class for the FunctionCommentSinceTagSniff sniff. - */ -final class FunctionCommentSinceTagUnitTest extends AbstractSniffUnitTest { - - /** - * Returns the lines where errors should occur. - * - * @return array <int line number> => <int number of errors> - */ - public function getErrorList() { - // The sniff only supports PHP functions for now; it ignores class, trait, and interface methods. - return array( - 9 => 1, - 19 => 1, - 24 => 1, - ); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array <int line number> => <int number of warnings> - */ - public function getWarningList() { - return array(); - } -} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.inc b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.inc new file mode 100644 index 0000000000000..aa8593b2e2757 --- /dev/null +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.inc @@ -0,0 +1,629 @@ +<?php +function foo() { + $result = do_action( 'some_filter', array() ); + $result = do_action_ref_array( 'some_filter', array() ); + $result = apply_filters( 'some_filter', array() ); + $result = apply_filters_ref_array( 'some_filter', array() ); + + /** + * @since + * 123 + */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since invalid 123 + */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since 123 + */ + $result = apply_filters( 'some_filter', array() ); + + $variable = 'some_value'; /** Some comment @since 123 */ + + $result = apply_filters( 'some_filter', array() ); + + /** @var This filter is documented in foo.php */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ + $result = apply_filters( 'some_filter', array() ); +} + +/** + * @since + * 123 + */ +function bar() { +} + +/** + * @since invalid 123 + */ +function baz() { +} + +/** + * @since 123 + */ +function qux() { +} + +$variable = 'some_value'; /** Some comment @since 123 */ + +function quux() { +} + +/** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ +function grault() { +} + +$result = array_map( function ( $value ) { + return $value * 2; // Doubling each value +}, array( 1, 2, 3, 4, 5 ) ); + +class Foo { + + /** + * @since + * 123 + */ + public $foo_property; + + /** + * @since invalid 123 + */ + protected $bar_property; + + /** + * @since 123 + */ + public $baz_property; + + public $qux_property = 'some_value'; /** Some comment @since 123 */ + + public $quux_property; + + private $corge_property; + + /** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ + public $grault_property; + + protected function foobar() { + $result = do_action( 'some_filter', array() ); + $result = do_action_ref_array( 'some_filter', array() ); + $result = apply_filters( 'some_filter', array() ); + $result = apply_filters_ref_array( 'some_filter', array() ); + + /** + * @since + * 123 + */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since invalid 123 + */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since 123 + */ + $result = apply_filters( 'some_filter', array() ); + + $variable = 'some_value'; /** Some comment @since 123 */ + + $result = apply_filters( 'some_filter', array() ); + + /** @var This filter is documented in foo.php */ + $result = apply_filters( 'some_filter', array() ); + } + + protected function foobar_1() {} + + /** + * @since + * 123 + */ + public function bar() { + } + + /** + * @since invalid 123 + */ + public function baz() { + } + + /** + * @since 123 + */ + protected function qux() { + } + + /** Some comment @since 123 */ + + public function quux() { + $result = array_map( function ( $value ) { + return $value * 2; // Doubling each value + }, array( 1, 2, 3, 4, 5 ) ); + } + + private function corge() { + } + + function no_visibility_modifier() {} + + /** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ + public function grault() { + } +} + +/** + * @since + * 123 + */ +class Foo_1 { +} + +/** + * @since invalid 123 + */ +class Foo_2 { +} + +/** + * @since 123 + */ +class Foo_3 { +} + +$variable = 'some_value'; /** Some comment @since 123 */ + +class Foo_4 { +} + +/** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ +class Foo_5 { +} + +abstract class Bar_Abstract { + /** + * @since + * 123 + */ + public $foo_property; + + /** + * @since invalid 123 + */ + protected $bar_property; + + /** + * @since 123 + */ + public $baz_property; + + public $qux_property = 'some_value'; /** Some comment @since 123 */ + + public $quux_property; + + private $corge_property; + + /** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ + public $grault_property; + + protected function foobar() { + $result = do_action( 'some_filter', array() ); + $result = do_action_ref_array( 'some_filter', array() ); + $result = apply_filters( 'some_filter', array() ); + $result = apply_filters_ref_array( 'some_filter', array() ); + + /** + * @since + * 123 + */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since invalid 123 + */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since 123 + */ + $result = apply_filters( 'some_filter', array() ); + + $variable = 'some_value'; /** Some comment @since 123 */ + + $result = apply_filters( 'some_filter', array() ); + + /** @var This filter is documented in foo.php */ + $result = apply_filters( 'some_filter', array() ); + } + + abstract protected function foo_abstract(); + + /** + * @since + * 123 + */ + public function apply_filters() { + } + + /** + * @since invalid 123 + */ + public function baz() { + } + + /** + * @since 123 + */ + protected function qux() { + } + + /** Some comment @since 123 */ + + public function quux() { + $result = array_map( function ( $value ) { + return $value * 2; // Doubling each value + }, array( 1, 2, 3, 4, 5 ) ); + } + + private function corge() { + } + + function no_visibility_modifier() {} + + /** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ + public function grault() { + } +} + +/** + * @since + * 123 + */ +abstract class Bar_1 { +} + +/** + * @since invalid 123 + */ +abstract class Bar_2 { +} + +/** + * @since 123 + */ +abstract class Bar_3 { +} + +$variable = 'some_value'; /** Some comment @since 123 */ + +abstract class Bar_4 { +} + +/** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ +abstract class Bar_5 { +} + +Trait Baz { + + /** + * @since + * 123 + */ + public $foo_property; + + /** + * @since invalid 123 + */ + protected $bar_property; + + /** + * @since 123 + */ + public $baz_property; + + public $qux_property = 'some_value'; /** Some comment @since 123 */ + + public $quux_property; + + private $corge_property; + + /** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ + public $grault_property; + + protected function foobar() { + $result = do_action( 'some_filter', array() ); + $result = do_action_ref_array( 'some_filter', array() ); + $result = apply_filters( 'some_filter', array() ); + $result = apply_filters_ref_array( 'some_filter', array() ); + + /** + * @since + * 123 + */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since invalid 123 + */ + $result = apply_filters( 'some_filter', array() ); + + /** + * @since 123 + */ + $result = apply_filters( 'some_filter', array() ); + + $variable = 'some_value'; /** Some comment @since 123 */ + + $result = apply_filters( 'some_filter', array() ); + + /** @var This filter is documented in foo.php */ + $result = apply_filters( 'some_filter', array() ); + } + + /** + * @since + * 123 + */ + public function bar() { + } + + /** + * @since invalid 123 + */ + public function baz() { + } + + /** + * @since 123 + */ + protected function qux() { + } + + /** Some comment @since 123 */ + + public function quux() { + $result = array_map( function ( $value ) { + return $value * 2; // Doubling each value + }, array( 1, 2, 3, 4, 5 ) ); + } + + private function corge() { + } + + function no_visibility_modifier() {} + + /** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ + public function grault() { + } +} + +/** + * @since + * 123 + */ +trait Baz_1 { +} + +/** + * @since invalid 123 + */ +trait Baz_2 { +} + +/** + * @since 123 + */ +trait Baz_3 { +} + +$variable = 'some_value'; /** Some comment @since 123 */ + +trait Baz_4 { +} + +interface Qux { + public function foobar(); + + /** + * @since + * 123 + */ + public function bar(); + + /** + * @since invalid 123 + */ + public function baz(); + + /** + * @since 123 + */ + public function qux(); + + /** Some comment @since 123 */ + + public function quux(); + + /** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ + public function grault(); +} + +/** + * @since + * 123 + */ +interface Qux_1 { +} + +/** + * @since invalid 123 + */ +interface Qux_2 { +} + +/** + * @since 123 + */ +interface Qux_3 { +} + +$variable = 'some_value'; /** Some comment @since 123 */ + +interface Qux_4 { +} + +$result = do_action( 'some_filter', array() ); +$result = do_action_ref_array( 'some_filter', array() ); +$result = apply_filters( 'some_filter', array() ); +$result = apply_filters_ref_array( 'some_filter', array() ); + +/** + * @since + * 123 + */ +$result = apply_filters( 'some_filter', array() ); + +/** + * @since invalid 123 + */ +$result = apply_filters( 'some_filter', array() ); + +/** + * @since 123 + */ +$result = apply_filters( 'some_filter', array() ); + +$variable = 'some_value'; /** Some comment @since 123 */$ + +$result = apply_filters( 'some_filter', array() ); + +/** @var This filter is documented in foo.php */ +$result = apply_filters( 'some_filter', array() ); + +/** + * @since 3.0.0 + * @since + * @since MU (3.0.0) + * @since invalid_value + * @since + */ +$result = apply_filters( 'some_filter', array() ); + +if ( ! function_exists( 'do_action' ) ) { + function do_action() { + } +} + +if ( ! function_exists( 'do_action_ref_array' ) ) { + function do_action_ref_array() { + } +} + +if ( ! function_exists( 'do_action_deprecated' ) ) { + function do_action_deprecated() { + } +} + +if ( ! function_exists( 'apply_filters' ) ) { + function apply_filters() { + } +} + +if ( ! function_exists( 'apply_filters_ref_array' ) ) { + function apply_filters_ref_array() { + } +} + +if ( ! function_exists( 'apply_filters_deprecated' ) ) { + function apply_filters_deprecated() { + } +} + +$foo = new do_action(); +$foo->do_action(); +$foo = new do_action_ref_array(); +$foo->do_action_ref_array(); +$foo = new do_action_deprecated(); +$foo->do_action_deprecated(); +$foo = new apply_filters(); +$foo->apply_filters(); +$foo = new apply_filters_ref_array(); +$foo->apply_filters_ref_array(); +$foo = new apply_filters_deprecated(); +$foo->apply_filters_deprecated(); +$foo = new non_hook_action(); +$foo->non_hook_action(); diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php new file mode 100644 index 0000000000000..bc7ca28c263ff --- /dev/null +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php @@ -0,0 +1,189 @@ +<?php +/** + * Unit test class for Gutenberg Coding Standard. + * + * @package gutenberg-coding-standards/gbc + * @link https://github.com/WordPress/gutenberg + * @license https://opensource.org/licenses/MIT MIT + */ + +namespace GutenbergCS\Gutenberg\Tests\Commenting; + +use GutenbergCS\Gutenberg\Sniffs\Commenting\SinceTagSniff; +use GutenbergCS\Gutenberg\Tests\AbstractSniffUnitTest; +use PHP_CodeSniffer\Sniffs\Sniff; + +/** + * Unit test class for the SinceTagSniff sniff. + */ +final class SinceTagUnitTest extends AbstractSniffUnitTest { + + /** + * Returns the lines where errors should occur. + * + * @return array <int line number> => <int number of errors> + */ + public function getErrorList() { + return array( + 2 => 1, + 3 => 1, + 4 => 1, + 5 => 1, + 6 => 1, + 9 => 1, + 15 => 1, + 26 => 1, + 33 => 1, + 35 => 1, + 36 => 1, + 42 => 1, + 49 => 1, + 62 => 1, + 67 => 1, + 69 => 1, + 70 => 1, + 79 => 1, + 82 => 1, + 88 => 1, + 97 => 1, + 99 => 1, + 105 => 1, + 107 => 1, + 108 => 1, + 112 => 1, + 113 => 1, + 114 => 1, + 115 => 1, + 116 => 1, + 119 => 1, + 125 => 1, + 136 => 1, + 142 => 1, + 145 => 1, + 152 => 1, + 165 => 1, + 174 => 1, + 178 => 1, + 180 => 1, + 181 => 1, + 188 => 1, + 195 => 1, + 208 => 1, + 213 => 1, + 215 => 1, + 216 => 1, + 221 => 1, + 223 => 1, + 229 => 1, + 238 => 1, + 240 => 1, + 246 => 1, + 248 => 1, + 249 => 1, + 253 => 1, + 254 => 1, + 255 => 1, + 256 => 1, + 257 => 1, + 260 => 1, + 266 => 1, + 277 => 1, + 283 => 1, + 286 => 1, + 293 => 1, + 306 => 1, + 315 => 1, + 319 => 1, + 321 => 1, + 322 => 1, + 329 => 1, + 336 => 1, + 349 => 1, + 354 => 1, + 356 => 1, + 357 => 1, + 362 => 1, + 365 => 1, + 371 => 1, + 380 => 1, + 382 => 1, + 388 => 1, + 390 => 1, + 391 => 1, + 395 => 1, + 396 => 1, + 397 => 1, + 398 => 1, + 399 => 1, + 402 => 1, + 408 => 1, + 419 => 1, + 426 => 1, + 433 => 1, + 446 => 1, + 455 => 1, + 459 => 1, + 461 => 1, + 462 => 1, + 469 => 1, + 476 => 1, + 489 => 1, + 492 => 1, + 493 => 1, + 496 => 1, + 502 => 1, + 513 => 1, + 517 => 1, + 519 => 1, + 520 => 1, + 526 => 1, + 533 => 1, + 546 => 1, + 549 => 1, + 550 => 1, + 551 => 1, + 552 => 1, + 555 => 1, + 561 => 1, + 572 => 1, + 579 => 1, + 581 => 1, + 582 => 1, + 587 => 1, + 592 => 1, + 597 => 1, + 602 => 1, + 607 => 1, + 612 => 1, + ); + } + + /** + * Returns the lines where warnings should occur. + * + * @return array <int line number> => <int number of warnings> + */ + public function getWarningList() { + return array(); + } + + /** + * Returns the fully qualified class name (FQCN) of the sniff. + * + * @return string The fully qualified class name of the sniff. + */ + protected function get_sniff_fqcn() { + return SinceTagSniff::class; + } + + /** + * Sets the parameters for the sniff. + * + * @throws RuntimeException If unable to set the ruleset parameters required for the test. + * + * @param Sniff $sniff The sniff being tested. + */ + public function set_sniff_parameters( Sniff $sniff ) { + $sniff->minimumVisibility = 'protected'; + } +} diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php index 794a088b7bc61..51174dd769d0a 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php @@ -10,22 +10,14 @@ namespace GutenbergCS\Gutenberg\Tests\NamingConventions; use GutenbergCS\Gutenberg\Sniffs\NamingConventions\ValidBlockLibraryFunctionNameSniff; -use PHP_CodeSniffer\Config; -use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use PHP_CodeSniffer\Ruleset; +use GutenbergCS\Gutenberg\Tests\AbstractSniffUnitTest; +use PHP_CodeSniffer\Sniffs\Sniff; /** * Unit test class for the ValidBlockLibraryFunctionNameSniff sniff. */ final class ValidBlockLibraryFunctionNameUnitTest extends AbstractSniffUnitTest { - /** - * Holds the original Ruleset instance. - * - * @var Ruleset - */ - private static $original_ruleset; - /** * Returns the lines where errors should occur. * @@ -50,50 +42,22 @@ public function getWarningList() { } /** + * Returns the fully qualified class name (FQCN) of the sniff. * - * This method resets the 'Gutenberg' ruleset in the $GLOBALS['PHP_CODESNIFFER_RULESETS'] - * to its original state. + * @return string The fully qualified class name of the sniff. */ - public static function tearDownAfterClass() { - parent::tearDownAfterClass(); - - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = self::$original_ruleset; - self::$original_ruleset = null; + protected function get_sniff_fqcn() { + return ValidBlockLibraryFunctionNameSniff::class; } - /** - * Prepares the environment before executing tests. Specifically, sets prefixes for the - * ValidBlockLibraryFunctionName sniff.This is needed since AbstractSniffUnitTest class - * doesn't apply sniff properties from the Gutenberg/ruleset.xml file. + * Sets the parameters for the sniff. * - * @param string $filename The name of the file being tested. - * @param Config $config The config data for the run. + * @throws RuntimeException If unable to set the ruleset parameters required for the test. * - * @return void + * @param Sniff $sniff The sniff being tested. */ - public function setCliValues( $filename, $config ) { - parent::setCliValues( $filename, $config ); - - if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] ) - || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - // Backup the original Ruleset instance. - self::$original_ruleset = $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg']; - - $current_ruleset = clone self::$original_ruleset; - $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] = $current_ruleset; - - if ( ! isset( $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ] ) - || ( ! $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ] instanceof ValidBlockLibraryFunctionNameSniff ) - ) { - throw new \RuntimeException( 'Cannot set ruleset parameters required for this test.' ); - } - - $sniff = $current_ruleset->sniffs[ ValidBlockLibraryFunctionNameSniff::class ]; + public function set_sniff_parameters( Sniff $sniff ) { $sniff->prefixes = array( 'block_core_', 'render_block_core_', diff --git a/test/php/gutenberg-coding-standards/Gutenberg/ruleset.xml b/test/php/gutenberg-coding-standards/Gutenberg/ruleset.xml index 74400e0e6d5cd..503f7e09b85d7 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/ruleset.xml +++ b/test/php/gutenberg-coding-standards/Gutenberg/ruleset.xml @@ -6,5 +6,6 @@ <rule ref="Gutenberg.CodeAnalysis.GuardedFunctionAndClassNames"/> <rule ref="Gutenberg.CodeAnalysis.ForbiddenFunctionsAndClasses"/> <rule ref="Gutenberg.NamingConventions.ValidBlockLibraryFunctionName"/> + <rule ref="Gutenberg.Commenting.SinceTag"/> </ruleset> diff --git a/test/php/gutenberg-coding-standards/composer.json b/test/php/gutenberg-coding-standards/composer.json index 0aeec177918c0..c1c27f81818aa 100644 --- a/test/php/gutenberg-coding-standards/composer.json +++ b/test/php/gutenberg-coding-standards/composer.json @@ -20,7 +20,8 @@ "ext-libxml": "*", "ext-tokenizer": "*", "ext-xmlreader": "*", - "squizlabs/php_codesniffer": "^3.7.2" + "squizlabs/php_codesniffer": "^3.7.2", + "phpcsstandards/phpcsutils": "^1.0.8" }, "require-dev": { "phpcompatibility/php-compatibility": "^9.0",